feat: rework document table filters

This commit is contained in:
Ephraim Atta-Duncan
2025-05-30 18:17:50 +00:00
parent 93aece9644
commit f5365554ab
14 changed files with 1407 additions and 102 deletions

View File

@ -0,0 +1,152 @@
import * as React from 'react';
import type { Column } from '@tanstack/react-table';
import { Check } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Badge } from '../badge';
import { Button } from '../button';
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '../command';
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
import { Separator } from '../separator';
interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
icon?: React.ComponentType<{ className?: string }>;
stats?: Record<string, number>;
onFilterChange?: (values: string[]) => void;
selectedValues?: string[];
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
color?: string;
bgColor?: string;
}[];
}
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
icon: Icon,
stats,
onFilterChange,
selectedValues,
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValuesSet = new Set(selectedValues || (column?.getFilterValue() as string[]));
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 gap-1.5 border-dashed px-2.5">
{Icon && <Icon className="size-4" />}
{title}
{selectedValuesSet.size > 0 && (
<>
<Separator orientation="vertical" className="mx-1 h-8" />
<Badge variant="secondary" className="rounded-sm px-2 py-0.5 font-normal lg:hidden">
{selectedValuesSet.size}
</Badge>
<div className="hidden gap-1 lg:flex">
{selectedValuesSet.size > 2 ? (
<Badge variant="neutral" className="rounded-sm px-2 py-0.5 font-normal">
{selectedValuesSet.size} selected
</Badge>
) : (
options
.filter((option) => selectedValuesSet.has(option.value))
.map((option) => (
<Badge
variant="neutral"
key={option.value}
className={cn(
'rounded-sm border-none px-2 py-0.5 font-normal',
option.bgColor ? option.bgColor : 'variant-secondary',
)}
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValuesSet.has(option.value);
return (
<CommandItem
key={option.value}
className="gap-x-2"
onSelect={() => {
if (isSelected) {
selectedValuesSet.delete(option.value);
} else {
selectedValuesSet.add(option.value);
}
const filterValues = Array.from(selectedValuesSet);
if (onFilterChange) {
onFilterChange(filterValues);
} else {
column?.setFilterValue(filterValues.length ? filterValues : undefined);
}
}}
>
<div
className={cn(
'flex size-4 items-center justify-center rounded-[4px] border',
isSelected
? 'bg-primary border-primary text-primary-foreground'
: 'border-input [&_svg]:invisible',
)}
>
<Check className="text-primary-foreground size-3.5" />
</div>
{option.icon && (
<option.icon
className={cn(
'size-4',
option.color ? option.color : 'text-muted-foreground',
)}
/>
)}
<span>{option.label}</span>
{(stats?.[option.value] || facets?.get(option.value)) && (
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
{stats?.[option.value] || facets?.get(option.value)}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{/* Option to clear filters, disabled for now since it makes the ui clanky. */}
{/* {selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)} */}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,83 @@
import type { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { Button } from '../button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
return (
<div className="flex items-center justify-between px-2">
<div className="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 25, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft />
</Button>
<Button
variant="outline"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft />
</Button>
<Button
variant="outline"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight />
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight />
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,137 @@
import * as React from 'react';
import type { Column } from '@tanstack/react-table';
import { cn } from '../../lib/utils';
import { Badge } from '../badge';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
} from '../select';
import { Separator } from '../separator';
interface DataTableSingleFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
icon?: React.ComponentType<{ className?: string }>;
onFilterChange?: (values: string[]) => void;
selectedValues?: string[];
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
color?: string;
bgColor?: string;
}[];
groups?: {
label: string;
values: string[];
}[];
}
export function DataTableSingleFilter<TData, TValue>({
column,
title,
options,
groups,
icon: Icon,
onFilterChange,
selectedValues,
}: DataTableSingleFilterProps<TData, TValue>) {
const filterValue = column?.getFilterValue() as string[] | undefined;
const selectedValue =
selectedValues?.[0] || (filterValue && filterValue.length > 0 ? filterValue[0] : undefined);
const selectedOption = options.find((option) => option.value === selectedValue);
const handleValueChange = (value: string) => {
if (value === selectedValue) {
if (onFilterChange) {
onFilterChange([]);
} else {
column?.setFilterValue(undefined);
}
} else {
if (onFilterChange) {
onFilterChange([value]);
} else {
column?.setFilterValue([value]);
}
}
};
const renderOptions = () => {
if (groups) {
return groups.map((group, groupIndex) => (
<React.Fragment key={group.label}>
<SelectGroup>
<SelectLabel>{group.label}</SelectLabel>
{options
.filter((option) => group.values.includes(option.value))
.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center gap-x-2">
{option.icon && (
<option.icon
className={cn(
'size-4',
option.color ? option.color : 'text-muted-foreground',
)}
/>
)}
<span>{option.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
{groupIndex < groups.length - 1 && <SelectSeparator />}
</React.Fragment>
));
}
return (
<SelectGroup>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center gap-x-2">
{option.icon && (
<option.icon
className={cn('size-4', option.color ? option.color : 'text-muted-foreground')}
/>
)}
<span>{option.label}</span>
</div>
</SelectItem>
))}
</SelectGroup>
);
};
return (
<Select value={selectedValue || ''} onValueChange={handleValueChange}>
<SelectTrigger className="border-input bg-background hover:bg-accent hover:text-accent-foreground h-8 w-auto gap-1.5 border border-dashed px-2.5 focus:ring-0">
{Icon && <Icon className="size-4" />}
{title}
{selectedValue && selectedOption && (
<>
<Separator orientation="vertical" className="mx-1 h-8" />
<Badge
variant="neutral"
className={cn(
'rounded-sm border-none px-2 py-0.5 font-normal',
selectedOption.bgColor ? selectedOption.bgColor : 'variant-secondary',
)}
>
{selectedOption.label}
</Badge>
</>
)}
</SelectTrigger>
<SelectContent>{renderOptions()}</SelectContent>
</Select>
);
}

View File

@ -0,0 +1,103 @@
import type { Table } from '@tanstack/react-table';
import { Calendar, CircleDashedIcon, ListFilter, X, XCircle } from 'lucide-react';
import { Button } from '../button';
import { Input } from '../input';
import { DataTableFacetedFilter } from './data-table-faceted-filter';
import { DataTableSingleFilter } from './data-table-single-filter';
import { statuses, timePeriodGroups, timePeriods } from './data/data';
interface DataTableToolbarProps<TData> {
table: Table<TData>;
stats?: Record<string, number>;
onStatusFilterChange?: (values: string[]) => void;
selectedStatusValues?: string[];
onTimePeriodFilterChange?: (values: string[]) => void;
selectedTimePeriodValues?: string[];
onResetFilters?: () => void;
isStatusFiltered?: boolean;
isTimePeriodFiltered?: boolean;
}
export function DataTableToolbar<TData>({
table,
stats,
onStatusFilterChange,
selectedStatusValues,
onTimePeriodFilterChange,
selectedTimePeriodValues,
onResetFilters,
isStatusFiltered,
isTimePeriodFiltered,
}: DataTableToolbarProps<TData>) {
const isFiltered =
table.getState().columnFilters.length > 0 || isStatusFiltered || isTimePeriodFiltered;
const searchValue = (table.getColumn('title')?.getFilterValue() as string) ?? '';
const handleClearFilter = () => {
table.getColumn('title')?.setFilterValue('');
};
const handleReset = () => {
table.resetColumnFilters();
onResetFilters?.();
};
return (
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center gap-2">
<div className="relative">
<Input
className="peer h-8 w-[150px] pe-9 ps-9 lg:w-[250px]"
placeholder="Search documents..."
value={searchValue}
onChange={(event) => table.getColumn('title')?.setFilterValue(event.target.value)}
/>
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50">
<ListFilter size={16} aria-hidden="true" />
</div>
{searchValue && (
<button
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md outline-none transition-[color,box-shadow] focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Clear filter"
onClick={handleClearFilter}
>
<XCircle className="size-3" aria-hidden="true" />
</button>
)}
</div>
{table.getColumn('status') && (
<DataTableFacetedFilter
column={table.getColumn('status')}
title="Status"
options={statuses}
icon={CircleDashedIcon}
stats={stats}
onFilterChange={onStatusFilterChange}
selectedValues={selectedStatusValues}
/>
)}
{table.getColumn('createdAt') && (
<DataTableSingleFilter
column={table.getColumn('createdAt')}
title="Time Period"
options={timePeriods}
groups={timePeriodGroups}
icon={Calendar}
onFilterChange={onTimePeriodFilterChange}
selectedValues={selectedTimePeriodValues}
/>
)}
{isFiltered && (
<Button variant="ghost" className="h-8 gap-2" size="sm" onClick={handleReset}>
Reset
<X className="size-4" />
</Button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,209 @@
import * as React from 'react';
import { useMemo } from 'react';
import { Trans } from '@lingui/react/macro';
import type {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
Updater,
VisibilityState,
} from '@tanstack/react-table';
import {
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import type { DataTableChildren } from '../data-table';
import { Skeleton } from '../skeleton';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../table';
import { DataTableToolbar } from './data-table-toolbar';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
columnVisibility?: VisibilityState;
perPage?: number;
currentPage?: number;
totalPages?: number;
onPaginationChange?: (_page: number, _perPage: number) => void;
children?: DataTableChildren<TData>;
stats?: Record<string, number>;
onStatusFilterChange?: (values: string[]) => void;
selectedStatusValues?: string[];
onTimePeriodFilterChange?: (values: string[]) => void;
selectedTimePeriodValues?: string[];
onResetFilters?: () => void;
isStatusFiltered?: boolean;
isTimePeriodFiltered?: boolean;
skeleton?: {
enable: boolean;
rows: number;
component?: React.ReactNode;
};
error?: {
enable: boolean;
component?: React.ReactNode;
};
}
export function DataTable<TData, TValue>({
columns,
data,
error,
perPage,
currentPage,
totalPages,
skeleton,
onPaginationChange,
children,
stats,
onStatusFilterChange,
selectedStatusValues,
onTimePeriodFilterChange,
selectedTimePeriodValues,
onResetFilters,
isStatusFiltered,
isTimePeriodFiltered,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const pagination = useMemo<PaginationState>(() => {
if (currentPage !== undefined && perPage !== undefined) {
return {
pageIndex: currentPage - 1,
pageSize: perPage,
};
}
return {
pageIndex: 0,
pageSize: 0,
};
}, [currentPage, perPage]);
const manualPagination = Boolean(currentPage !== undefined && totalPages !== undefined);
const onTablePaginationChange = (updater: Updater<PaginationState>) => {
if (typeof updater === 'function') {
const newState = updater(pagination);
onPaginationChange?.(newState.pageIndex + 1, newState.pageSize);
} else {
onPaginationChange?.(updater.pageIndex + 1, updater.pageSize);
}
};
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination: manualPagination ? pagination : undefined,
},
manualPagination,
pageCount: totalPages,
initialState: {
pagination: {
pageSize: 10,
},
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
onPaginationChange: onTablePaginationChange,
});
return (
<>
<div className="flex flex-col gap-4">
<DataTableToolbar
table={table}
stats={stats}
onStatusFilterChange={onStatusFilterChange}
selectedStatusValues={selectedStatusValues}
onTimePeriodFilterChange={onTimePeriodFilterChange}
selectedTimePeriodValues={selectedTimePeriodValues}
onResetFilters={onResetFilters}
isStatusFiltered={isStatusFiltered}
isTimePeriodFiltered={isTimePeriodFiltered}
/>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : error?.enable ? (
<TableRow>
{error.component ?? (
<TableCell colSpan={columns.length} className="h-32 text-center">
<Trans>Something went wrong.</Trans>
</TableCell>
)}
</TableRow>
) : skeleton?.enable ? (
Array.from({ length: skeleton.rows }).map((_, i) => (
<TableRow key={`skeleton-row-${i}`}>
{skeleton.component ?? <Skeleton />}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
{children && <div className="mt-8 w-full">{children(table)}</div>}
</>
);
}

View File

@ -0,0 +1,101 @@
import { CheckCircle2, Clock, File, Inbox, XCircle } from 'lucide-react';
export const statuses = [
{
value: 'DRAFT',
label: 'Draft',
icon: File,
color: 'text-yellow-500 dark:text-yellow-300',
bgColor: 'bg-yellow-100 dark:bg-yellow-100 text-yellow-700 dark:text-yellow-700',
},
{
value: 'PENDING',
label: 'Pending',
icon: Clock,
color: 'text-water-700 dark:text-water-300',
bgColor: 'bg-water-100 dark:bg-water-100 text-water-700 dark:text-water-700',
},
{
value: 'COMPLETED',
label: 'Completed',
icon: CheckCircle2,
color: 'text-documenso-700 dark:text-documenso-300',
bgColor: 'bg-documenso-200 dark:bg-documenso-200 text-documenso-800 dark:text-documenso-800',
},
{
value: 'REJECTED',
label: 'Rejected',
icon: XCircle,
color: 'text-red-700 dark:text-red-300',
bgColor: 'bg-red-100 dark:bg-red-100 text-red-500 dark:text-red-700',
},
{
value: 'INBOX',
label: 'Inbox',
icon: Inbox,
color: 'text-blue-700 dark:text-blue-300',
bgColor: 'bg-blue-100 dark:bg-blue-100 text-blue-700 dark:text-blue-700',
},
];
export const timePeriods = [
{
value: 'today',
label: 'Today',
},
{
value: 'this-week',
label: 'This Week',
},
{
value: 'this-month',
label: 'This Month',
},
{
value: 'this-quarter',
label: 'This Quarter',
},
{
value: 'this-year',
label: 'This Year',
},
{
value: 'yesterday',
label: 'Yesterday',
},
{
value: 'last-week',
label: 'Last Week',
},
{
value: 'last-month',
label: 'Last Month',
},
{
value: 'last-quarter',
label: 'Last Quarter',
},
{
value: 'last-year',
label: 'Last Year',
},
{
value: 'all-time',
label: 'All Time',
},
];
export const timePeriodGroups = [
{
label: 'Present',
values: ['today', 'this-week', 'this-month', 'this-quarter', 'this-year'],
},
{
label: 'Past',
values: ['yesterday', 'last-week', 'last-month', 'last-quarter', 'last-year'],
},
{
label: '',
values: ['all-time'],
},
];

View File

@ -0,0 +1,56 @@
import { Avatar, AvatarFallback, AvatarImage } from '../avatar';
import { Button } from '../button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '../dropdown-menu';
export function UserNav() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">shadcn</p>
<p className="text-muted-foreground text-xs leading-none">m@example.com</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,113 @@
import { DateTime } from 'luxon';
export type TimePeriod =
| 'today'
| 'this-week'
| 'this-month'
| 'this-quarter'
| 'this-year'
| 'yesterday'
| 'last-week'
| 'last-month'
| 'last-quarter'
| 'last-year'
| 'all-time';
export function getDateRangeForPeriod(
period: TimePeriod,
): { start: DateTime; end: DateTime } | null {
const now = DateTime.now();
switch (period) {
case 'today':
return {
start: now.startOf('day'),
end: now.endOf('day'),
};
case 'yesterday': {
const yesterday = now.minus({ days: 1 });
return {
start: yesterday.startOf('day'),
end: yesterday.endOf('day'),
};
}
case 'this-week':
return {
start: now.startOf('week'),
end: now.endOf('week'),
};
case 'last-week': {
const lastWeek = now.minus({ weeks: 1 });
return {
start: lastWeek.startOf('week'),
end: lastWeek.endOf('week'),
};
}
case 'this-month':
return {
start: now.startOf('month'),
end: now.endOf('month'),
};
case 'last-month': {
const lastMonth = now.minus({ months: 1 });
return {
start: lastMonth.startOf('month'),
end: lastMonth.endOf('month'),
};
}
case 'this-quarter':
return {
start: now.startOf('quarter'),
end: now.endOf('quarter'),
};
case 'last-quarter': {
const lastQuarter = now.minus({ quarters: 1 });
return {
start: lastQuarter.startOf('quarter'),
end: lastQuarter.endOf('quarter'),
};
}
case 'this-year':
return {
start: now.startOf('year'),
end: now.endOf('year'),
};
case 'last-year': {
const lastYear = now.minus({ years: 1 });
return {
start: lastYear.startOf('year'),
end: lastYear.endOf('year'),
};
}
case 'all-time':
return null;
default:
return null;
}
}
export function isDateInPeriod(date: Date, period: TimePeriod): boolean {
const dateTime = DateTime.fromJSDate(date);
if (period === 'all-time') {
return true;
}
const range = getDateRangeForPeriod(period);
if (!range) {
return true;
}
return dateTime >= range.start && dateTime <= range.end;
}