diff --git a/.agents/knowledge-base.md b/.agents/knowledge-base.md index ce55849012..36d25bc53f 100644 --- a/.agents/knowledge-base.md +++ b/.agents/knowledge-base.md @@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/ Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency: - Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx` -- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx` +- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx` - Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views ## When working on workflow triggers or similar enum-based features in the Cal.com codebase diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx index 21379a8a6f..dfb17af3ee 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx @@ -12,7 +12,7 @@ import { MembershipRole } from "@calcom/prisma/enums"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import { validStatuses } from "~/bookings/lib/validStatuses"; -import BookingsList from "~/bookings/views/bookings-listing-view"; +import BookingsList from "~/bookings/views/bookings-view"; const querySchema = z.object({ status: z.enum(validStatuses), diff --git a/apps/web/modules/bookings/components/BookingsCalendar.tsx b/apps/web/modules/bookings/components/BookingsCalendar.tsx new file mode 100644 index 0000000000..eb1cf6b6c5 --- /dev/null +++ b/apps/web/modules/bookings/components/BookingsCalendar.tsx @@ -0,0 +1,34 @@ +"use client"; + +import type { Table as ReactTable } from "@tanstack/react-table"; + +import { DataTableFilters, DataTableSegment } from "@calcom/features/data-table"; +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +import type { RowData, BookingListingStatus } from "../types"; + +type BookingsCalendarViewProps = { + status: BookingListingStatus; + table: ReactTable; +}; + +export function BookingsCalendar({ table }: BookingsCalendarViewProps) { + return ( + <> +
+
+ +
+ +
+ + + +
+
+
+ +
+ + ); +} diff --git a/apps/web/modules/bookings/components/BookingsList.tsx b/apps/web/modules/bookings/components/BookingsList.tsx new file mode 100644 index 0000000000..ae4ad90067 --- /dev/null +++ b/apps/web/modules/bookings/components/BookingsList.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { Table as ReactTable } from "@tanstack/react-table"; + +import { DataTableWrapper, DataTableFilters, DataTableSegment } from "@calcom/features/data-table"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { EmptyScreen } from "@calcom/ui/components/empty-screen"; + +import SkeletonLoader from "@components/booking/SkeletonLoader"; + +import type { RowData, BookingListingStatus } from "../types"; + +const descriptionByStatus: Record = { + upcoming: "upcoming_bookings", + recurring: "recurring_bookings", + past: "past_bookings", + cancelled: "cancelled_bookings", + unconfirmed: "unconfirmed_bookings", +}; + +type BookingsListViewProps = { + status: BookingListingStatus; + table: ReactTable; + isPending: boolean; + totalRowCount?: number; +}; + +export function BookingsList({ status, table, isPending, totalRowCount }: BookingsListViewProps) { + const { t } = useLocale(); + + return ( + + + + } + ToolbarRight={ + <> + + + + + } + LoaderView={} + EmptyView={ +
+ +
+ } + /> + ); +} diff --git a/apps/web/modules/bookings/types.ts b/apps/web/modules/bookings/types.ts new file mode 100644 index 0000000000..68bcaa935a --- /dev/null +++ b/apps/web/modules/bookings/types.ts @@ -0,0 +1,25 @@ +import type { RouterOutputs } from "@calcom/trpc/react"; + +import type { validStatuses } from "~/bookings/lib/validStatuses"; + +export type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; + +export type RecurringInfo = { + recurringEventId: string | null; + count: number; + firstDate: Date | null; + bookings: { [key: string]: Date[] }; +}; + +export type RowData = + | { + type: "data"; + booking: BookingOutput; + isToday: boolean; + recurringInfo?: RecurringInfo; + } + | { + type: "today" | "next"; + }; + +export type BookingListingStatus = (typeof validStatuses)[number]; diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-view.tsx similarity index 82% rename from apps/web/modules/bookings/views/bookings-listing-view.tsx rename to apps/web/modules/bookings/views/bookings-view.tsx index c4ccbab26e..00931e1a27 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-view.tsx @@ -2,57 +2,38 @@ import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table"; import { useSearchParams, usePathname } from "next/navigation"; -import { useMemo, useRef } from "react"; +import { createParser, useQueryState } from "nuqs"; +import { useMemo } from "react"; import dayjs from "@calcom/dayjs"; import { - useDataTable, DataTableProvider, - DataTableWrapper, - DataTableFilters, - DataTableSegment, + type SystemFilterSegment, + useDataTable, ColumnFilterType, useFilterValue, ZMultiSelectFilterValue, ZDateRangeFilterValue, ZTextFilterValue, - type SystemFilterSegment, } from "@calcom/features/data-table"; import { useSegments } from "@calcom/features/data-table/hooks/useSegments"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { Alert } from "@calcom/ui/components/alert"; -import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation"; import { HorizontalTabs } from "@calcom/ui/components/navigation"; import type { VerticalTabItemProps } from "@calcom/ui/components/navigation"; import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton"; import BookingListItem from "@components/booking/BookingListItem"; -import SkeletonLoader from "@components/booking/SkeletonLoader"; import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues"; import type { validStatuses } from "~/bookings/lib/validStatuses"; -type BookingListingStatus = (typeof validStatuses)[number]; -type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0]; - -type RecurringInfo = { - recurringEventId: string | null; - count: number; - firstDate: Date | null; - bookings: { [key: string]: Date[] }; -}; - -const descriptionByStatus: Record = { - upcoming: "upcoming_bookings", - recurring: "recurring_bookings", - past: "past_bookings", - cancelled: "cancelled_bookings", - unconfirmed: "unconfirmed_bookings", -}; +import { BookingsCalendar } from "../components/BookingsCalendar"; +import { BookingsList } from "../components/BookingsList"; +import type { RowData, BookingOutput } from "../types"; type BookingsProps = { status: (typeof validStatuses)[number]; @@ -103,21 +84,18 @@ export default function Bookings(props: BookingsProps) { ); } -type RowData = - | { - type: "data"; - booking: BookingOutput; - isToday: boolean; - recurringInfo?: RecurringInfo; - } - | { - type: "today" | "next"; - }; +const viewParser = createParser({ + parse: (value: string) => { + if (value === "calendar") return "calendar"; + return "list"; + }, + serialize: (value: "list" | "calendar") => value, +}); function BookingsContent({ status, permissions }: BookingsProps) { + const [view] = useQueryState("view", viewParser.withDefault("list")); const { t } = useLocale(); const user = useMeQuery().data; - const tableContainerRef = useRef(null); const searchParams = useSearchParams(); // Generate dynamic tabs that preserve query parameters @@ -411,6 +389,9 @@ function BookingsContent({ status, permissions }: BookingsProps) { getFacetedUniqueValues, }); + const isPending = query.isPending; + const totalRowCount = query.data?.totalCount; + return (
@@ -431,43 +412,16 @@ function BookingsContent({ status, permissions }: BookingsProps) { {!!bookingsToday.length && status === "upcoming" && ( )} - - - - } - ToolbarRight={ - <> - - - - - } - LoaderView={} - EmptyView={ -
- -
- } - /> + {view === "list" ? ( + + ) : ( + + )} )}
diff --git a/packages/features/data-table/GUIDE.md b/packages/features/data-table/GUIDE.md index fb4be41df3..c1d28d8324 100644 --- a/packages/features/data-table/GUIDE.md +++ b/packages/features/data-table/GUIDE.md @@ -976,7 +976,7 @@ From `packages/features/users/components/UserTable/UserListTable.tsx`: ### Example 2: Bookings List -From `apps/web/modules/bookings/views/bookings-listing-view.tsx`: +From `apps/web/modules/bookings/components/BookingsList.tsx`: ```tsx