From aaaff0705b6095e88c79c19209528346f316d1f5 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 17 Dec 2025 14:47:51 +0100 Subject: [PATCH] fix(bookings): enable past date selection for cancelled bookings (#25644) * refactor(data-table): clean up DateRangeFilter range options - Replace 'past' | 'custom' with 'past' | 'future' | 'any' | 'customOnly' - Add direction field to PresetOption for preset compatibility filtering - Derive presets visibility automatically based on compatible presets - Update bookings list to use new range values: - past -> 'past' - upcoming -> 'future' - unconfirmed/recurring/cancelled -> 'any' Co-Authored-By: eunjae@cal.com * feat(playground): add DateRangeFilter playground page with E2E tests - Add playground page at /settings/admin/playground/date-range-filter - Demonstrate all 4 range options: past, future, any, customOnly - Add link to playground index page - Add E2E tests for presets visibility and date restrictions Co-Authored-By: eunjae@cal.com * fix(playground): use correct meta.filter pattern for column filter config Co-Authored-By: eunjae@cal.com * clean up the playground esign * add unit tests instead of e2e * fix the implementation --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../playground/date-range-filter/page.tsx | 150 ++++++++++++++++++ .../(admin-layout)/admin/playground/page.tsx | 40 ++++- .../modules/bookings/columns/filterColumns.ts | 2 +- .../bookings/hooks/useBookingListColumns.tsx | 2 +- .../components/filters/DateRangeFilter.tsx | 32 ++-- .../features/data-table/lib/dateRange.test.ts | 52 ++++++ packages/features/data-table/lib/dateRange.ts | 29 +++- packages/features/data-table/lib/types.ts | 2 +- packages/types/data-table.d.ts | 4 +- 9 files changed, 283 insertions(+), 30 deletions(-) create mode 100644 apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/date-range-filter/page.tsx diff --git a/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/date-range-filter/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/date-range-filter/page.tsx new file mode 100644 index 0000000000..3ee39e2700 --- /dev/null +++ b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/date-range-filter/page.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useReactTable, getCoreRowModel, createColumnHelper } from "@tanstack/react-table"; +import { useMemo } from "react"; + +import { ColumnFilterType, DateRangeFilter, DataTableProvider } from "@calcom/features/data-table"; +import type { DateRangeFilterOptions } from "@calcom/features/data-table/lib/types"; + +type DemoRow = { + id: number; + name: string; + date: string; +}; + +const columnHelper = createColumnHelper(); + +type ScenarioProps = { + id: string; + title: string; + description: string; + expected: string; + range: DateRangeFilterOptions["range"]; +}; + +const scenarios: ScenarioProps[] = [ + { + id: "past", + title: 'Range: "past"', + description: "Restricts date selection to past dates only. Shows presets that are past-compatible.", + expected: + "Presets visible: Today, Last 7 days, Last 30 days, Month to date, Year to date, Custom. Calendar maxDate = today.", + range: "past", + }, + { + id: "future", + title: 'Range: "future"', + description: "Restricts date selection to future dates only. Shows only future-compatible presets.", + expected: "Presets visible: Custom only (presets with direction 'any'). Calendar minDate = today.", + range: "future", + }, + { + id: "any", + title: 'Range: "any"', + description: "No date restrictions. Shows all presets.", + expected: "All presets visible. No calendar date restrictions.", + range: "any", + }, + { + id: "customOnly", + title: 'Range: "customOnly"', + description: "Forces custom date picker only. Always hides presets dropdown.", + expected: "No presets dropdown. Only calendar picker visible when opened. No date restrictions.", + range: "customOnly", + }, +]; + +function ScenarioCard({ scenario }: { scenario: ScenarioProps }) { + const columns = useMemo( + () => [ + columnHelper.accessor("name", { + header: "Name", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("date", { + id: "dateRange", + header: "Date Range", + cell: (info) => info.getValue(), + enableColumnFilter: true, + meta: { + filter: { + type: ColumnFilterType.DATE_RANGE, + dateRangeOptions: { + range: scenario.range, + }, + }, + }, + }), + ], + [scenario.range] + ); + + const data = useMemo( + () => [ + { id: 1, name: "Demo Item 1", date: "2024-01-15" }, + { id: 2, name: "Demo Item 2", date: "2024-02-20" }, + ], + [] + ); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + // Get the column definition to pass to DateRangeFilter + const dateRangeColumn = table.getAllColumns().find((col) => col.id === "dateRange"); + const columnMeta = dateRangeColumn?.columnDef.meta as + | { filter?: { type: string; dateRangeOptions?: DateRangeFilterOptions } } + | undefined; + const dateRangeOptions = columnMeta?.filter?.dateRangeOptions; + + return ( +
+

{scenario.title}

+

{scenario.description}

+

+ Expected: {scenario.expected} +

+ +
+ + {dateRangeColumn && ( + + )} + +
+
+ ); +} + +export default function DateRangeFilterPlayground() { + return ( +
+
+

DateRangeFilter Playground

+

+ This page demonstrates the different range options for the DateRangeFilter component. +

+

+ The range option controls both date restrictions and presets visibility. Presets + visibility is derived automatically based on compatible presets. +

+
+ + {scenarios.map((scenario) => ( + + ))} +
+ ); +} diff --git a/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/page.tsx index 628d2f3453..47d408be7a 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(admin-layout)/admin/playground/page.tsx @@ -2,19 +2,32 @@ import { _generateMetadata, getTranslate } from "app/_utils"; import Link from "next/link"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { Icon } from "@calcom/ui/components/icon"; const LINKS = [ { title: "Routing Funnel", + description: "Visualize booking conversion flow and routing patterns", href: "/settings/admin/playground/routing-funnel", + icon: "filter" as const, }, { title: "Bookings by Hour", + description: "View booking distribution across different hours", href: "/settings/admin/playground/bookings-by-hour", + icon: "chart-bar" as const, }, { title: "Weekly Calendar", + description: "Interactive weekly calendar view for scheduling", href: "/settings/admin/playground/weekly-calendar", + icon: "calendar" as const, + }, + { + title: "Date Range Filter", + description: "Test date range selection and filtering components", + href: "/settings/admin/playground/date-range-filter", + icon: "calendar-days" as const, }, ]; @@ -31,16 +44,27 @@ const Page = async () => { const t = await getTranslate(); return ( -
-
    +
    +
    {LINKS.map((link) => ( -
  • - - {link.title} → - -
  • + +
    + +
    +
    +

    {link.title}

    +

    {link.description}

    +
    + + ))} -
+
); diff --git a/apps/web/modules/bookings/columns/filterColumns.ts b/apps/web/modules/bookings/columns/filterColumns.ts index 7ed2b73275..1c74c9c6ef 100644 --- a/apps/web/modules/bookings/columns/filterColumns.ts +++ b/apps/web/modules/bookings/columns/filterColumns.ts @@ -109,7 +109,7 @@ export function buildFilterColumns({ t, permissions, status }: BuildFilterColumn filter: { type: ColumnFilterType.DATE_RANGE, dateRangeOptions: { - range: status === "past" ? "past" : "custom", + range: status === "past" ? "past" : status === "cancelled" ? "any" : "future", // upcoming, unconfirmed, recurring are all future-only }, }, }, diff --git a/apps/web/modules/bookings/hooks/useBookingListColumns.tsx b/apps/web/modules/bookings/hooks/useBookingListColumns.tsx index 1a0d8e2fb5..dd0cfbccdf 100644 --- a/apps/web/modules/bookings/hooks/useBookingListColumns.tsx +++ b/apps/web/modules/bookings/hooks/useBookingListColumns.tsx @@ -98,7 +98,7 @@ export function useBookingListColumns({ filter: { type: ColumnFilterType.DATE_RANGE, dateRangeOptions: { - range: status === "past" ? "past" : "custom", + range: status === "past" ? "past" : status === "cancelled" ? "any" : "future", // upcoming, unconfirmed, recurring are all future-only }, }, }, diff --git a/packages/features/data-table/components/filters/DateRangeFilter.tsx b/packages/features/data-table/components/filters/DateRangeFilter.tsx index b4378e7246..cb03b3f73f 100644 --- a/packages/features/data-table/components/filters/DateRangeFilter.tsx +++ b/packages/features/data-table/components/filters/DateRangeFilter.tsx @@ -24,10 +24,10 @@ import { CUSTOM_PRESET, CUSTOM_PRESET_VALUE, DEFAULT_PRESET, - PRESET_OPTIONS, getDefaultStartDate, getDefaultEndDate, getDateRangeFromPreset, + getCompatiblePresets, type PresetOption, } from "../../lib/dateRange"; import { preserveLocalTime } from "../../lib/preserveLocalTime"; @@ -53,8 +53,10 @@ export const DateRangeFilter = ({ const filterValue = useFilterValue(column.id, ZDateRangeFilterValue); const { updateFilter, removeFilter, timeZone: givenTimeZone } = useDataTable(); const range = options?.range ?? "past"; - const forceCustom = range === "custom"; - const forcePast = range === "past"; + + const compatiblePresets = getCompatiblePresets(range); + const showPresets = compatiblePresets.length > 1; + const forceCustomOnly = range === "customOnly" || !showPresets; const { t } = useLocale(); const currentDate = dayjs(); @@ -65,10 +67,10 @@ export const DateRangeFilter = ({ filterValue?.data.endDate ? dayjs(filterValue.data.endDate) : undefined ); const [selectedPreset, setSelectedPreset] = useState( - forceCustom + forceCustomOnly ? CUSTOM_PRESET : filterValue?.data.preset - ? PRESET_OPTIONS.find((o) => o.value === filterValue.data.preset) ?? DEFAULT_PRESET + ? compatiblePresets.find((o) => o.value === filterValue.data.preset) ?? DEFAULT_PRESET : DEFAULT_PRESET ); @@ -108,14 +110,14 @@ export const DateRangeFilter = ({ useEffect(() => { // initially apply the default value // if the query param is not set yet - if (!filterValue && !forceCustom) { + if (!filterValue && !forceCustomOnly) { updateValues({ preset: DEFAULT_PRESET, startDate: getDefaultStartDate(), endDate: getDefaultEndDate(), }); } - }, [filterValue, forceCustom, updateValues]); + }, [filterValue, forceCustomOnly, updateValues]); const updateDateRangeFromPreset = (val: string | null) => { if (val === CUSTOM_PRESET_VALUE) { @@ -195,13 +197,19 @@ export const DateRangeFilter = ({ endDate: endDate?.toDate(), }} data-testid="date-range-calendar" - minDate={forcePast ? currentDate.subtract(2, "year").toDate() : null} - maxDate={forcePast ? currentDate.toDate() : undefined} + minDate={ + range === "past" + ? currentDate.subtract(2, "year").toDate() + : range === "future" + ? currentDate.toDate() + : null + } + maxDate={range === "past" ? currentDate.toDate() : undefined} disabled={false} onDatesChange={updateDateRangeFromPicker} withoutPopover={true} /> - {forceCustom && ( + {forceCustomOnly && (