diff --git a/apps/web/modules/bookings/components/BookingListContainer.tsx b/apps/web/modules/bookings/components/BookingListContainer.tsx index 23e3566267..cf1bdf2738 100644 --- a/apps/web/modules/bookings/components/BookingListContainer.tsx +++ b/apps/web/modules/bookings/components/BookingListContainer.tsx @@ -1,19 +1,7 @@ "use client"; -import { - useReactTable, - getCoreRowModel, - getSortedRowModel, -} from "@tanstack/react-table"; -import { useRouter } from "next/navigation"; -import React, { useState, useMemo, useEffect, useCallback } from "react"; - import dayjs from "@calcom/dayjs"; -import { - useDataTable, - useDisplayedFilterCount, -} from "@calcom/features/data-table"; -import { DataTableSegment, DataTableFilters } from "~/data-table/components"; +import { useDataTable, useDisplayedFilterCount } from "@calcom/features/data-table"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; @@ -22,7 +10,9 @@ import { Badge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; import { ToggleGroup } from "@calcom/ui/components/form"; import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton"; - +import { getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useBookingFilters } from "~/bookings/hooks/useBookingFilters"; import { useBookingListColumns } from "~/bookings/hooks/useBookingListColumns"; import { useBookingListData } from "~/bookings/hooks/useBookingListData"; @@ -30,18 +20,15 @@ import { useBookingStatusTab } from "~/bookings/hooks/useBookingStatusTab"; import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues"; import { useListAutoSelector } from "~/bookings/hooks/useListAutoSelector"; import { useListNavigationCapabilities } from "~/bookings/hooks/useListNavigationCapabilities"; - +import { DataTableFilters, DataTableSegment } from "~/data-table/components"; import { BookingDetailsSheetStoreProvider, useBookingDetailsSheetStore, } from "../store/bookingDetailsSheetStore"; -import type { - RowData, - BookingListingStatus, - BookingsGetOutput, -} from "../types"; +import type { BookingListingStatus, BookingsGetOutput, RowData } from "../types"; import { BookingDetailsSheet } from "./BookingDetailsSheet"; import { BookingList } from "./BookingList"; +import { BookingsCsvDownload } from "./BookingsCsvDownload"; import { ViewToggleButton } from "./ViewToggleButton"; interface FilterButtonProps { @@ -50,11 +37,7 @@ interface FilterButtonProps { setShowFilters: (value: boolean | ((prev: boolean) => boolean)) => void; } -function FilterButton({ - table, - displayedFilterCount, - setShowFilters, -}: FilterButtonProps) { +function FilterButton({ table, displayedFilterCount, setShowFilters }: FilterButtonProps) { const { t } = useLocale(); if (displayedFilterCount === 0) { @@ -67,8 +50,7 @@ function FilterButton({ StartIcon="list-filter" className="h-full" size="sm" - onClick={() => setShowFilters((value) => !value)} - > + onClick={() => setShowFilters((value) => !value)}> {t("filter")} {displayedFilterCount} @@ -109,9 +91,7 @@ function BookingListInner({ }: BookingListInnerProps) { const { t } = useLocale(); const user = useMeQuery().data; - const setSelectedBookingUid = useBookingDetailsSheetStore( - (state) => state.setSelectedBookingUid - ); + const setSelectedBookingUid = useBookingDetailsSheetStore((state) => state.setSelectedBookingUid); const router = useRouter(); const [showFilters, setShowFilters] = useState(true); @@ -119,11 +99,7 @@ function BookingListInner({ useListAutoSelector(bookings); const ErrorView = errorMessage ? ( - + ) : undefined; const handleBookingClick = useCallback( @@ -190,9 +166,7 @@ function BookingListInner({ value={currentTab} onValueChange={(value) => { if (!value) return; - const selectedTab = tabOptions.find( - (tab) => tab.value === value - ); + const selectedTab = tabOptions.find((tab) => tab.value === value); if (selectedTab?.href) { router.push(selectedTab.href); } @@ -213,9 +187,8 @@ function BookingListInner({
- {bookingsV3Enabled && ( - - )} + + {bookingsV3Enabled && }
{displayedFilterCount > 0 && showFilters && (
@@ -230,11 +203,7 @@ function BookingListInner({
)} {status === "upcoming" && !isEmpty && ( - + )}
query.data?.bookings ?? [], - [query.data?.bookings] - ); + const bookings = useMemo(() => query.data?.bookings ?? [], [query.data?.bookings]); // Always call the hook and provide navigation capabilities // The BookingDetailsSheet is only rendered when bookingsV3Enabled is true (see line 212) @@ -330,10 +285,7 @@ export function BookingListContainer(props: BookingListContainerProps) { }); return ( - + string; + +const BATCH_SIZE = 100; + +interface BookingsCsvDownloadProps { + status: BookingListingStatus; +} + +function transformBookingToCsv(booking: BookingOutput, t: TranslationFunction) { + return { + [t("booking_uid")]: booking.uid, + [t("title")]: booking.title, + [t("status")]: booking.status, + [t("start_time")]: dayjs(booking.startTime).format("YYYY-MM-DD HH:mm:ss"), + [t("end_time")]: dayjs(booking.endTime).format("YYYY-MM-DD HH:mm:ss"), + [t("attendee_name")]: booking.attendees.map((a) => a.name).join("; "), + [t("email")]: booking.attendees.map((a) => a.email).join("; "), + [t("event_type")]: booking.eventType?.title ?? "", + [t("location")]: booking.location ?? "", + }; +} + +export function BookingsCsvDownload({ status }: BookingsCsvDownloadProps) { + const { t } = useLocale(); + const { data: user, isPending: isUserPending } = useMeQuery(); + const [isDownloading, setIsDownloading] = useState(false); + const utils = trpc.useUtils(); + + const { eventTypeIds, teamIds, userIds, dateRange, attendeeName, attendeeEmail, bookingUid } = + useBookingFilters(); + + // Only show for users who are part of an organization + const isOrgUser = Boolean(user?.organizationId); + + if (isUserPending || !isOrgUser) { + return null; + } + + const fetchBatch = async (offset: number) => { + const result = await utils.viewer.bookings.get.fetch({ + limit: BATCH_SIZE, + offset, + filters: { + statuses: [status], + eventTypeIds, + teamIds, + userIds, + attendeeName, + attendeeEmail, + bookingUid, + afterStartDate: dateRange?.startDate + ? dayjs(dateRange?.startDate).startOf("day").toISOString() + : undefined, + beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined, + }, + }); + + return { + bookings: result.bookings, + totalCount: result.totalCount, + }; + }; + + const handleDownload = async () => { + try { + setIsDownloading(true); + showProgressToast(0); + + // Fetch first batch to get total count + const firstBatch = await fetchBatch(0); + let allBookings = firstBatch.bookings; + const totalCount = firstBatch.totalCount; + + // Continue fetching remaining batches + while (allBookings.length < totalCount) { + const offset = allBookings.length; + const batch = await fetchBatch(offset); + if (batch.bookings.length === 0) break; // Prevent infinite loop if batch returns empty + allBookings = [...allBookings, ...batch.bookings]; + + const currentProgress = Math.min(Math.round((allBookings.length / totalCount) * 100), 99); + showProgressToast(currentProgress); + } + + showProgressToast(100); + + // Transform and download + const csvData = allBookings.map((booking) => transformBookingToCsv(booking, t)); + const filename = `${t("bookings").toLowerCase()}-${status}-${dayjs().format("YYYY-MM-DD")}.csv`; + downloadAsCsv(csvData, filename); + } catch { + showToast(t("unexpected_error_try_again"), "error"); + } finally { + setIsDownloading(false); + hideProgressToast(); + } + }; + + return ( + + ); +}