* fix: deep link reschedule audit log to booking drawer history tab Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: make booking drawer tab-agnostic for cross-tab deep links Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use status-agnostic /bookings URL for audit log deep links - Update audit service URLs from /bookings/upcoming?uid=... to /bookings?uid=... - Add /bookings/page.tsx redirect that routes to /bookings/upcoming preserving query params - Update tests to expect new URL format Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * refactor: use client-side replaceState instead of server redirect for booking deep links Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use router.replace instead of replaceState to update tab and booking list Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * improvements * refactor: extract deep link logic from BookingListContainer into usePreSelectedBooking hook Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use proper BookingOutput status type in test helper Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: decouple getTabForBooking from BookingOutput type for simpler testing Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: accept Date | string for endTime in BookingForTabResolution interface Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: eliminate initialBookingUid prop drilling and revert formatting-only changes Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: use AuditDeepLink wrapper to preserve target=_blank through ServerTrans cloneElement Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Improve code organization * refactor: rename usePreSelectedBooking to useSwitchToCorrectStatusTab Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: push preSelectedBooking into store so drawer opens on direct navigation Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * fixes * fixes * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { useDataTable } from "~/data-table/hooks/useDataTable";
|
|
import { useDisplayedFilterCount } from "~/data-table/hooks/useDisplayedFilterCount";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
|
import { Alert } from "@calcom/ui/components/alert";
|
|
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 { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useBookingFilters } from "~/bookings/hooks/useBookingFilters";
|
|
import { useBookingListColumns } from "~/bookings/hooks/useBookingListColumns";
|
|
import { useBookingListData } from "~/bookings/hooks/useBookingListData";
|
|
import { useBookingStatusTab } from "~/bookings/hooks/useBookingStatusTab";
|
|
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
|
|
import { useListAutoSelector } from "~/bookings/hooks/useListAutoSelector";
|
|
import { useSwitchToCorrectStatusTab } from "~/bookings/hooks/useSwitchToCorrectStatusTab";
|
|
import { DataTableFilters, DataTableSegment } from "~/data-table/components";
|
|
import {
|
|
BookingDetailsSheetStoreProvider,
|
|
useBookingDetailsSheetStore,
|
|
} from "../store/bookingDetailsSheetStore";
|
|
import type { BookingListingStatus, BookingsGetOutput, RowData } from "../types";
|
|
import { BookingDetailsSheet } from "./BookingDetailsSheet";
|
|
import { BookingList } from "./BookingList";
|
|
import { ViewToggleButton } from "./ViewToggleButton";
|
|
|
|
interface FilterButtonProps {
|
|
table: ReturnType<typeof useReactTable<RowData>>;
|
|
displayedFilterCount: number;
|
|
setShowFilters: (value: boolean | ((prev: boolean) => boolean)) => void;
|
|
}
|
|
|
|
function FilterButton({ table, displayedFilterCount, setShowFilters }: FilterButtonProps) {
|
|
const { t } = useLocale();
|
|
|
|
if (displayedFilterCount === 0) {
|
|
return <DataTableFilters.AddFilterButton table={table} />;
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
color="secondary"
|
|
StartIcon="list-filter"
|
|
className="h-full"
|
|
size="sm"
|
|
onClick={() => setShowFilters((value) => !value)}>
|
|
{t("filter")}
|
|
<Badge variant="gray" className="ml-1">
|
|
{displayedFilterCount}
|
|
</Badge>
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
interface BookingListContainerProps {
|
|
status: BookingListingStatus;
|
|
permissions: {
|
|
canReadOthersBookings: boolean;
|
|
};
|
|
bookingsV3Enabled: boolean;
|
|
bookingAuditEnabled: boolean;
|
|
}
|
|
|
|
interface BookingListInnerProps extends BookingListContainerProps {
|
|
data?: BookingsGetOutput;
|
|
isPending: boolean;
|
|
hasError: boolean;
|
|
errorMessage?: string;
|
|
totalRowCount?: number;
|
|
bookings: BookingsGetOutput["bookings"];
|
|
}
|
|
|
|
function BookingListInner({
|
|
status,
|
|
permissions,
|
|
bookings,
|
|
bookingsV3Enabled,
|
|
bookingAuditEnabled,
|
|
data,
|
|
isPending,
|
|
hasError,
|
|
errorMessage,
|
|
totalRowCount,
|
|
}: BookingListInnerProps) {
|
|
const { t } = useLocale();
|
|
const user = useMeQuery().data;
|
|
const setSelectedBookingUid = useBookingDetailsSheetStore((state) => state.setSelectedBookingUid);
|
|
const router = useRouter();
|
|
const [showFilters, setShowFilters] = useState(true);
|
|
|
|
// Handle auto-selection for list view
|
|
useListAutoSelector(bookings);
|
|
|
|
const ErrorView = errorMessage ? (
|
|
<Alert severity="error" title={t("something_went_wrong")} message={errorMessage} />
|
|
) : undefined;
|
|
|
|
const handleBookingClick = useCallback(
|
|
(bookingUid: string) => {
|
|
setSelectedBookingUid(bookingUid);
|
|
},
|
|
[setSelectedBookingUid]
|
|
);
|
|
|
|
const columns = useBookingListColumns({
|
|
user,
|
|
status,
|
|
canReadOthersBookings: permissions.canReadOthersBookings,
|
|
bookingsV3Enabled,
|
|
handleBookingClick,
|
|
});
|
|
|
|
const finalData = useBookingListData({
|
|
data,
|
|
status,
|
|
userTimeZone: user?.timeZone,
|
|
});
|
|
|
|
const getFacetedUniqueValues = useFacetedUniqueValues({
|
|
canReadOthersBookings: permissions.canReadOthersBookings,
|
|
});
|
|
|
|
const displayedFilterCount = useDisplayedFilterCount();
|
|
const { currentTab, tabOptions } = useBookingStatusTab();
|
|
|
|
useEffect(() => {
|
|
if (displayedFilterCount === 0) {
|
|
// reset to true, so it shows filters as soon as any filter is applied
|
|
setShowFilters(true);
|
|
}
|
|
}, [displayedFilterCount]);
|
|
|
|
const table = useReactTable<RowData>({
|
|
data: finalData,
|
|
columns,
|
|
initialState: {
|
|
columnVisibility: {
|
|
eventTypeId: false,
|
|
teamId: false,
|
|
userId: false,
|
|
attendeeName: false,
|
|
attendeeEmail: false,
|
|
dateRange: false,
|
|
bookingUid: false,
|
|
},
|
|
},
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getFacetedUniqueValues,
|
|
});
|
|
|
|
const isEmpty = !data?.bookings || data.bookings.length === 0;
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{/* Desktop: full width on first row, Mobile: full width on first row with horizontal scroll */}
|
|
<div className="w-full md:w-auto">
|
|
<div className="overflow-x-auto md:overflow-visible">
|
|
<ToggleGroup
|
|
value={currentTab}
|
|
onValueChange={(value) => {
|
|
if (!value) return;
|
|
const selectedTab = tabOptions.find((tab) => tab.value === value);
|
|
if (selectedTab?.href) {
|
|
router.push(selectedTab.href);
|
|
}
|
|
}}
|
|
options={tabOptions}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop: second item on first row, Mobile: first item on second row */}
|
|
<FilterButton
|
|
table={table}
|
|
displayedFilterCount={displayedFilterCount}
|
|
setShowFilters={setShowFilters}
|
|
/>
|
|
|
|
{/* Desktop: auto-pushed to right via flex-grow spacer, Mobile: continue on second row */}
|
|
<div className="hidden grow md:block" />
|
|
|
|
<DataTableSegment.Select />
|
|
{/* <BookingsCsvDownload status={status} /> */}
|
|
{bookingsV3Enabled && <ViewToggleButton bookingsV3Enabled={bookingsV3Enabled} />}
|
|
</div>
|
|
{displayedFilterCount > 0 && showFilters && (
|
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
|
<DataTableFilters.ActiveFilters table={table} />
|
|
<DataTableFilters.AddFilterButton table={table} variant="minimal" />
|
|
|
|
{/* Desktop: auto-pushed to right via flex-grow spacer */}
|
|
<div className="hidden flex-grow md:block" />
|
|
|
|
<DataTableFilters.ClearFiltersButton />
|
|
<DataTableSegment.SaveButton />
|
|
</div>
|
|
)}
|
|
{status === "upcoming" && !isEmpty && (
|
|
<WipeMyCalActionButton className="mt-4" bookingStatus={status} bookingsEmpty={isEmpty} />
|
|
)}
|
|
<div className="mt-4">
|
|
<BookingList
|
|
status={status}
|
|
table={table}
|
|
isPending={isPending}
|
|
totalRowCount={totalRowCount}
|
|
ErrorView={ErrorView}
|
|
hasError={hasError}
|
|
/>
|
|
</div>
|
|
|
|
{bookingsV3Enabled && (
|
|
<BookingDetailsSheet
|
|
userTimeZone={user?.timeZone}
|
|
userTimeFormat={user?.timeFormat === null ? undefined : user?.timeFormat}
|
|
userId={user?.id}
|
|
userEmail={user?.email}
|
|
bookingAuditEnabled={bookingAuditEnabled}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function BookingListContainer(props: BookingListContainerProps) {
|
|
const { limit, offset, isValidatorPending } = useDataTable();
|
|
const { eventTypeIds, teamIds, userIds, dateRange, attendeeName, attendeeEmail, bookingUid } =
|
|
useBookingFilters();
|
|
|
|
const { resolvedTabStatus, isResolvingTabStatus, preSelectedBooking } = useSwitchToCorrectStatusTab({
|
|
defaultStatus: props.status,
|
|
});
|
|
|
|
// Build query input once - shared between query and prefetching
|
|
const queryInput = useMemo(
|
|
() => ({
|
|
limit,
|
|
offset,
|
|
filters: {
|
|
statuses: [resolvedTabStatus],
|
|
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,
|
|
},
|
|
}),
|
|
[
|
|
limit,
|
|
offset,
|
|
resolvedTabStatus,
|
|
eventTypeIds,
|
|
teamIds,
|
|
userIds,
|
|
attendeeName,
|
|
attendeeEmail,
|
|
bookingUid,
|
|
dateRange,
|
|
]
|
|
);
|
|
|
|
const query = trpc.viewer.bookings.get.useQuery(queryInput, {
|
|
staleTime: 5 * 60 * 1000, // 5 minutes - data is considered fresh
|
|
gcTime: 30 * 60 * 1000, // 30 minutes - cache retention time
|
|
// We wait for tab status to be resolved before fetching, so that we can fetch the correct bookings as per resolved tab status
|
|
enabled: !isValidatorPending && !isResolvingTabStatus, // Wait for validator to be ready before fetching
|
|
});
|
|
|
|
const bookings = useMemo(() => {
|
|
const queryBookings = query.data?.bookings ?? [];
|
|
if (!preSelectedBooking) return queryBookings;
|
|
if (queryBookings.some((b) => b.uid === preSelectedBooking.uid)) return queryBookings;
|
|
// It ensures that the drawer opens for a booking that isn't even in the bookings list
|
|
// Note that, bookings list doesn't use this so, it won't be visible in the list view but drawer will open for it
|
|
// We don't want to show this booking in the list view, as it might not match the filters/pagination applied
|
|
return [...queryBookings, preSelectedBooking];
|
|
}, [query.data?.bookings, preSelectedBooking]);
|
|
|
|
// Always call the hook and provide navigation capabilities
|
|
// The BookingDetailsSheet is only rendered when bookingsV3Enabled is true (see line 212)
|
|
// const capabilities = useListNavigationCapabilities({
|
|
// limit,
|
|
// offset,
|
|
// totalCount: query.data?.totalCount,
|
|
// setPageIndex,
|
|
// queryInput,
|
|
// });
|
|
|
|
return (
|
|
<BookingDetailsSheetStoreProvider bookings={bookings}>
|
|
<BookingListInner
|
|
{...props}
|
|
status={resolvedTabStatus}
|
|
data={query.data}
|
|
isPending={query.isPending}
|
|
hasError={!!query.error}
|
|
errorMessage={query.error?.message}
|
|
totalRowCount={query.data?.totalCount}
|
|
bookings={bookings}
|
|
/>
|
|
</BookingDetailsSheetStoreProvider>
|
|
);
|
|
}
|