diff --git a/apps/web/modules/bookings/hooks/useStretchedHeightToBottom.ts b/apps/web/modules/bookings/hooks/useStretchedHeightToBottom.ts deleted file mode 100644 index 003d099853..0000000000 --- a/apps/web/modules/bookings/hooks/useStretchedHeightToBottom.ts +++ /dev/null @@ -1,42 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import { debounce } from "lodash"; -import { useCallback, useEffect, useRef } from "react"; - -// Dynamically adjusts DataTable height on mobile to prevent nested scrolling -// and ensure the table fits within the viewport without overflowing (hacky) -export function useStretchedHeightToBottom(ref: React.RefObject) { - const lastOffsetY = useRef(); - const lastWindowHeight = useRef(); - const BOTTOM_NAV_HEIGHT = 64; - const BUFFER = 32; - - const updateHeight = useCallback( - debounce(() => { - if (!ref.current) return; - const rect = ref.current.getBoundingClientRect(); - if (rect.top !== lastOffsetY.current || window.innerHeight !== lastWindowHeight.current) { - lastOffsetY.current = rect.top; - lastWindowHeight.current = window.innerHeight; - let height = window.innerHeight - lastOffsetY.current - BUFFER; - if (window.innerWidth < 640) { - height = height - BOTTOM_NAV_HEIGHT; - } - ref.current.style.height = `${height}px`; - } - }, 200), - [ref.current] - ); - - useEffect(() => { - const handleResize = () => { - updateHeight(); - }; - - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [updateHeight]); - - updateHeight(); -} diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 5c7372262b..2a8c527a6a 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -12,6 +12,7 @@ import { useMemo, useRef } from "react"; import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components"; import dayjs from "@calcom/dayjs"; import { + useDataTable, DataTableProvider, DataTableWrapper, DataTableFilters, @@ -33,7 +34,6 @@ import BookingListItem from "@components/booking/BookingListItem"; import SkeletonLoader from "@components/booking/SkeletonLoader"; import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues"; -import { useStretchedHeightToBottom } from "~/bookings/hooks/useStretchedHeightToBottom"; import type { validStatuses } from "~/bookings/lib/validStatuses"; type BookingListingStatus = (typeof validStatuses)[number]; @@ -109,7 +109,6 @@ function BookingsContent({ status }: BookingsProps) { const { t } = useLocale(); const user = useMeQuery().data; const tableContainerRef = useRef(null); - useStretchedHeightToBottom(tableContainerRef); const eventTypeIds = useFilterValue("eventTypeId", ZMultiSelectFilterValue)?.data as number[] | undefined; const teamIds = useFilterValue("teamId", ZMultiSelectFilterValue)?.data as number[] | undefined; @@ -118,26 +117,24 @@ function BookingsContent({ status }: BookingsProps) { const attendeeName = useFilterValue("attendeeName", ZTextFilterValue); const attendeeEmail = useFilterValue("attendeeEmail", ZTextFilterValue); - const query = trpc.viewer.bookings.get.useInfiniteQuery( - { - limit: 10, - filters: { - status, - eventTypeIds, - teamIds, - userIds, - attendeeName, - attendeeEmail, - afterStartDate: dateRange?.startDate - ? dayjs(dateRange?.startDate).startOf("day").toISOString() - : undefined, - beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined, - }, + const { limit, offset } = useDataTable(); + + const query = trpc.viewer.bookings.get.useQuery({ + limit, + offset, + filters: { + status, + eventTypeIds, + teamIds, + userIds, + attendeeName, + attendeeEmail, + afterStartDate: dateRange?.startDate + ? dayjs(dateRange?.startDate).startOf("day").toISOString() + : undefined, + beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined, }, - { - getNextPageParam: (lastPage) => lastPage.nextCursor, - } - ); + }); const columns = useMemo(() => { const columnHelper = createColumnHelper(); @@ -262,7 +259,7 @@ function BookingsContent({ status }: BookingsProps) { ]; }, [user, status, t]); - const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]); + const isEmpty = useMemo(() => !query.data?.bookings.length, [query.data]); const flatData = useMemo(() => { const shownBookings: Record = {}; @@ -289,37 +286,33 @@ function BookingsContent({ status }: BookingsProps) { }; return ( - query.data?.pages.flatMap((page) => - page.bookings.filter(filterBookings).map((booking) => ({ - type: "data", - booking, - recurringInfo: page.recurringInfo.find( - (info) => info.recurringEventId === booking.recurringEventId - ), - isToday: false, - })) - ) || [] + query.data?.bookings.filter(filterBookings).map((booking) => ({ + type: "data", + booking, + recurringInfo: query.data?.recurringInfo.find( + (info) => info.recurringEventId === booking.recurringEventId + ), + isToday: false, + })) || [] ); }, [query.data]); const bookingsToday = useMemo(() => { return ( - query.data?.pages.flatMap((page) => - page.bookings - .filter( - (booking: BookingOutput) => - dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") === - dayjs().tz(user?.timeZone).format("YYYY-MM-DD") - ) - .map((booking) => ({ - type: "data" as const, - booking, - recurringInfo: page.recurringInfo.find( - (info) => info.recurringEventId === booking.recurringEventId - ), - isToday: true, - })) - ) || [] + query.data?.bookings + .filter( + (booking: BookingOutput) => + dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") === + dayjs().tz(user?.timeZone).format("YYYY-MM-DD") + ) + .map((booking) => ({ + type: "data" as const, + booking, + recurringInfo: query.data?.recurringInfo.find( + (info) => info.recurringEventId === booking.recurringEventId + ), + isToday: true, + })) ?? [] ); }, [query.data]); @@ -379,17 +372,16 @@ function BookingsContent({ status }: BookingsProps) { )} diff --git a/packages/features/data-table/components/DataTableWrapper.tsx b/packages/features/data-table/components/DataTableWrapper.tsx index d22675ea71..4c7a26cff1 100644 --- a/packages/features/data-table/components/DataTableWrapper.tsx +++ b/packages/features/data-table/components/DataTableWrapper.tsx @@ -12,7 +12,6 @@ import { useDataTable, useColumnFilters, } from "@calcom/features/data-table"; -import classNames from "@calcom/ui/classNames"; type BaseDataTableWrapperProps = { testId?: string; @@ -110,7 +109,7 @@ export function DataTableWrapper({ return ( <> {(ToolbarLeft || ToolbarRight || children) && ( -
+
{ToolbarLeft}
diff --git a/packages/lib/bookings/getAllUserBookings.ts b/packages/lib/bookings/getAllUserBookings.ts index 30ce939284..3125ac6320 100644 --- a/packages/lib/bookings/getAllUserBookings.ts +++ b/packages/lib/bookings/getAllUserBookings.ts @@ -77,7 +77,7 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, const combinedFilters = bookingListingByStatus.map((status) => bookingListingFilters[status]); - const { bookings, recurringInfo } = await getBookings({ + const { bookings, recurringInfo, totalCount } = await getBookings({ user, prisma, passedBookingsStatusFilter: { @@ -89,18 +89,10 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, skip, }); - const bookingsFetched = bookings.length; - let nextCursor: typeof skip | null = skip; - if (bookingsFetched > take) { - nextCursor += bookingsFetched; - } else { - nextCursor = null; - } - return { bookings, recurringInfo, - nextCursor, + totalCount, }; }; diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 36c5e785df..77f395b3f2 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -29,24 +29,24 @@ const log = logger.getSubLogger({ prefix: ["bookings.get"] }); export const getHandler = async ({ ctx, input }: GetOptions) => { // using offset actually because cursor pagination requires a unique column // for orderBy, but we don't use a unique column in our orderBy - const take = input.limit ?? 10; - const skip = input.cursor ?? 0; + const take = input.limit; + const skip = input.offset; const { prisma, user } = ctx; const defaultStatus = "upcoming"; const bookingListingByStatus = [input.filters.status || defaultStatus]; - const { bookings, recurringInfo, nextCursor } = await getAllUserBookings({ + const { bookings, recurringInfo, totalCount } = await getAllUserBookings({ ctx: { user: { id: user.id, email: user.email }, prisma: prisma }, bookingListingByStatus: bookingListingByStatus, - take: take, - skip: skip, + take, + skip, filters: input.filters, }); return { bookings, recurringInfo, - nextCursor, + totalCount, }; }; @@ -183,167 +183,174 @@ export async function getBookings({ getUserIdsWhereUserIsOrgAdminOrOwner(prisma, membershipConditionWhereUserIsAdminOwner), ]); - const plainBookings = await prisma.booking.findMany({ - where: { - OR: [ - { - userId: user.id, + const whereClause = { + OR: [ + { + userId: user.id, + }, + { + attendees: { + some: { + email: user.email, + }, }, - { - attendees: { - some: { + }, + { + eventTypeId: { + in: eventTypeIdsWhereUserIsAdminOrOwener, + }, + }, + { + userId: { + in: userIdsWhereUserIsOrgAdminOrOwener, + }, + }, + { + seatsReferences: { + some: { + attendee: { email: user.email, }, }, }, - { - eventTypeId: { - in: eventTypeIdsWhereUserIsAdminOrOwener, - }, - }, - { - userId: { - in: userIdsWhereUserIsOrgAdminOrOwener, - }, - }, - { - seatsReferences: { - some: { - attendee: { - email: user.email, + }, + ], + AND: [ + passedBookingsStatusFilter, + ...(eventTypeIdsFromTeamIdsFilter + ? [ + { + eventTypeId: { + in: eventTypeIdsFromTeamIdsFilter, }, }, - }, - }, - ], - AND: [ - passedBookingsStatusFilter, - ...(eventTypeIdsFromTeamIdsFilter - ? [ - { - eventTypeId: { - in: eventTypeIdsFromTeamIdsFilter, - }, - }, - ] - : []), - ...(filters?.userIds && filters.userIds.length > 0 - ? [ - { - OR: [ - { - userId: { - in: filters.userIds, - }, + ] + : []), + ...(filters?.userIds && filters.userIds.length > 0 + ? [ + { + OR: [ + { + userId: { + in: filters.userIds, }, - ...(attendeeEmailsFromUserIdsFilter?.length - ? [ - { - attendees: { - some: { - email: { - in: attendeeEmailsFromUserIdsFilter, - }, + }, + ...(attendeeEmailsFromUserIdsFilter?.length + ? [ + { + attendees: { + some: { + email: { + in: attendeeEmailsFromUserIdsFilter, }, }, }, - ] - : []), - ], - }, - ] - : []), - ...(eventTypeIdsFromEventTypeIdsFilter - ? [ - { - eventTypeId: { in: eventTypeIdsFromEventTypeIdsFilter }, - }, - ] - : []), + }, + ] + : []), + ], + }, + ] + : []), + ...(eventTypeIdsFromEventTypeIdsFilter + ? [ + { + eventTypeId: { in: eventTypeIdsFromEventTypeIdsFilter }, + }, + ] + : []), - ...(typeof filters?.attendeeEmail === "string" - ? [ - { - attendees: { some: { email: filters.attendeeEmail.trim() } }, + ...(typeof filters?.attendeeEmail === "string" + ? [ + { + attendees: { some: { email: filters.attendeeEmail.trim() } }, + }, + ] + : []), + ...(isTextFilterValue(filters?.attendeeEmail) + ? [ + { + attendees: { + some: makeWhereClause({ + columnName: "email", + filterValue: filters.attendeeEmail, + }), }, - ] - : []), - ...(isTextFilterValue(filters?.attendeeEmail) - ? [ - { - attendees: { - some: makeWhereClause({ - columnName: "email", - filterValue: filters.attendeeEmail, - }), - }, - }, - ] - : []), + }, + ] + : []), - ...(typeof filters?.attendeeName === "string" - ? [ - { - attendees: { some: { name: filters.attendeeName.trim() } }, + ...(typeof filters?.attendeeName === "string" + ? [ + { + attendees: { some: { name: filters.attendeeName.trim() } }, + }, + ] + : []), + ...(isTextFilterValue(filters?.attendeeName) + ? [ + { + attendees: { + some: makeWhereClause({ + columnName: "name", + filterValue: filters.attendeeName, + }), }, - ] - : []), - ...(isTextFilterValue(filters?.attendeeName) - ? [ - { - attendees: { - some: makeWhereClause({ - columnName: "name", - filterValue: filters.attendeeName, - }), - }, - }, - ] - : []), + }, + ] + : []), - ...(filters?.afterStartDate - ? [ - { - startTime: { - gte: dayjs.utc(filters.afterStartDate).toDate(), - }, + ...(filters?.afterStartDate + ? [ + { + startTime: { + gte: dayjs.utc(filters.afterStartDate).toDate(), }, - ] - : []), - ...(filters?.beforeEndDate - ? [ - { - endTime: { - lte: dayjs.utc(filters.beforeEndDate).toDate(), - }, + }, + ] + : []), + ...(filters?.beforeEndDate + ? [ + { + endTime: { + lte: dayjs.utc(filters.beforeEndDate).toDate(), }, - ] - : []), - ...(filters?.afterUpdatedDate - ? [ - { - updatedAt: { - gte: dayjs.utc(filters.afterUpdatedDate).toDate(), - }, + }, + ] + : []), + ...(filters?.afterUpdatedDate + ? [ + { + updatedAt: { + gte: dayjs.utc(filters.afterUpdatedDate).toDate(), }, - ] - : []), - ...(filters?.beforeUpdatedDate - ? [ - { - updatedAt: { - lte: dayjs.utc(filters.beforeUpdatedDate).toDate(), - }, + }, + ] + : []), + ...(filters?.beforeUpdatedDate + ? [ + { + updatedAt: { + lte: dayjs.utc(filters.beforeUpdatedDate).toDate(), }, - ] - : []), - ], - }, - select: bookingSelect, - orderBy, - take: take + 1, - skip, - }); + }, + ] + : []), + ], + }; + + const [plainBookings, totalCount] = await Promise.all([ + prisma.booking.findMany({ + where: whereClause, + select: bookingSelect, + orderBy, + take, + skip, + }), + prisma.booking.count({ + where: whereClause, + }), + ]); const [ recurringInfoBasic, @@ -446,7 +453,7 @@ export async function getBookings({ }; }) ); - return { bookings, recurringInfo }; + return { bookings, recurringInfo, totalCount }; } async function getEventTypeIdsFromTeamIdsFilter(prisma: PrismaClient, teamIds?: number[]) { diff --git a/packages/trpc/server/routers/viewer/bookings/get.schema.ts b/packages/trpc/server/routers/viewer/bookings/get.schema.ts index d282ec8043..f1c02a0b24 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.schema.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.schema.ts @@ -15,8 +15,8 @@ export const ZGetInputSchema = z.object({ afterUpdatedDate: z.string().optional(), beforeUpdatedDate: z.string().optional(), }), - limit: z.number().min(1).max(100).nullish(), - cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type + limit: z.number().min(1).max(100), + offset: z.number(), }); export type TGetInputSchema = z.infer;