Files
reui-blocks/blocks/data-table14.tsx
T

1147 lines
32 KiB
TypeScript

"use client";
import {
type Column,
type ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
RowSelectionState,
type SortingState,
type Table as TableType,
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
import {
ArrowDown,
ArrowUp,
CheckCheckIcon,
CheckCircle,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Clock,
Footprints,
ListFilterIcon,
LoaderIcon,
Package,
PackageIcon,
PieChart,
RefreshCcw,
Shirt,
ShoppingBag,
Smartphone,
} from "lucide-react";
import * as React from "react";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
const formatNumber = (value: number) => value.toLocaleString("fa-IR");
type UseDataTableOptions<TData> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, unknown>>;
getRowId?: (row: TData) => string;
initialSorting?: SortingState;
initialGlobalFilter?: string;
initialFilters?: ColumnFiltersState;
initialVisibility?: VisibilityState;
initialSelection?: RowSelectionState;
enableRowSelection?: boolean;
};
export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
const {
data,
columns,
getRowId,
initialSorting = [],
initialFilters = [],
initialGlobalFilter = "",
initialVisibility = {},
initialSelection = {},
enableRowSelection = true,
} = options;
const [sorting, setSorting] = React.useState<SortingState>(initialSorting);
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>(initialFilters);
const [globalFilter, setGlobalFilter] =
React.useState<string>(initialGlobalFilter);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>(initialVisibility);
const [rowSelection, setRowSelection] =
React.useState<RowSelectionState>(initialSelection);
const table = useReactTable({
data,
columns,
getRowId,
state: {
sorting,
columnFilters,
globalFilter,
columnVisibility,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onGlobalFilterChange: setGlobalFilter,
onColumnVisibilityChange: setColumnVisibility,
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
enableRowSelection,
});
return {
table,
columnFilters,
setColumnFilters,
sorting,
setSorting,
globalFilter,
setGlobalFilter,
columnVisibility,
setColumnVisibility,
rowSelection,
setRowSelection,
};
}
type DataTableColumnHeaderProps<TData, TValue> = {
column: Column<TData, TValue>;
title: string;
};
export const DataTableColumnHeader = <TData, TValue>({
column,
title,
}: DataTableColumnHeaderProps<TData, TValue>) => {
const canSort = column.getCanSort();
const sorted = column.getIsSorted();
if (!canSort) {
return (
<span className="flex h-8 items-center text-sm font-medium text-foreground">
{title}
</span>
);
}
return (
<Button
variant="ghost"
size="sm"
className="flex h-8 items-center gap-2 px-0 text-sm font-medium text-foreground"
onClick={() => column.toggleSorting(sorted === "asc")}
>
<span>{title}</span>
{sorted === "desc" ? (
<ArrowDown className="h-4 w-4" />
) : sorted === "asc" ? (
<ArrowUp className="h-4 w-4" />
) : (
<ChevronsUpDown className="h-4 w-4 opacity-50" />
)}
</Button>
);
};
type DataTablePaginationProps<TData> = {
table: TableType<TData>;
pageSizeOptions?: number[];
};
export const DataTablePagination = <TData,>({
table,
pageSizeOptions = [10, 20, 30, 40, 50],
}: DataTablePaginationProps<TData>) => {
const currentPage = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const totalRows = table.getFilteredRowModel().rows.length;
const pageCount = table.getPageCount();
const startRow = currentPage * pageSize + 1;
const endRow = Math.min((currentPage + 1) * pageSize, totalRows);
const relevantPageSizes = pageSizeOptions.filter(
(size) => size <= totalRows || size === pageSize,
);
return (
<div className="flex flex-col gap-4 px-2 py-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex-1 text-center text-sm text-muted-foreground sm:text-start">
<span className="font-medium">
{totalRows === 0 ? (
"نتیجه‌ای یافت نشد"
) : (
<>
نمایش {formatNumber(startRow)} تا {formatNumber(endRow)} از{" "}
{formatNumber(totalRows)} رکورد
</>
)}
</span>
</div>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
<div className="flex items-center gap-2">
<p className="text-sm font-medium whitespace-nowrap">تعداد در هر صفحه</p>
<Select
value={`${pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
disabled={totalRows === 0}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side="top">
{relevantPageSizes.map((size) => (
<SelectItem key={size} value={`${size}`}>
{formatNumber(size)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-4">
<div className="text-sm font-medium whitespace-nowrap">
صفحه {formatNumber(currentPage + 1)} از {formatNumber(pageCount)}
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={currentPage === 0 || totalRows === 0}
aria-label="رفتن به صفحه اول"
>
<ChevronsLeft className="h-4 w-4 rtl:rotate-180" />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="رفتن به قبلیی"
>
<ChevronLeft className="h-4 w-4 rtl:rotate-180" />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="رفتن به بعدیی"
>
<ChevronRight className="h-4 w-4 rtl:rotate-180" />
</Button>
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(pageCount - 1)}
disabled={currentPage === pageCount - 1 || totalRows === 0}
aria-label="رفتن به صفحه آخر"
>
<ChevronsRight className="h-4 w-4 rtl:rotate-180" />
</Button>
</div>
</div>
</div>
</div>
);
};
interface DataTableButtonFilterOption {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}
interface DataTableButtonFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
options: DataTableButtonFilterOption[];
align?: "start" | "center" | "end";
}
export function DataTableButtonFilter<TData, TValue>({
column,
title = "فیلتر",
options,
align = "start",
}: DataTableButtonFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const facetCounts = React.useMemo(() => {
if (!facets || !column) return null;
const map = new Map<string, number>();
facets.forEach((count, key) => {
const numeric = Number(count);
const stringKey = String(key);
map.set(stringKey, numeric);
map.set(stringKey.toLowerCase(), numeric);
});
return map;
}, [facets, column]);
const filterValue = column?.getFilterValue();
const selectedValues = React.useMemo(() => {
if (filterValue == null) return new Set<string>();
const values = Array.isArray(filterValue)
? filterValue.filter(Boolean).map(String)
: [String(filterValue)];
return new Set(values.map((value) => value.toLowerCase()));
}, [filterValue]);
const selectedCount = selectedValues.size;
const handleValueChange = React.useCallback(
(value: string, checked: boolean) => {
if (!column) return;
const currentValue = column.getFilterValue();
const raw = Array.isArray(currentValue)
? currentValue.map(String)
: currentValue
? [String(currentValue)]
: [];
const next = new Set(raw);
if (checked) {
next.add(value);
} else {
next.delete(value);
}
column.setFilterValue(next.size ? Array.from(next) : undefined);
},
[column],
);
const clearFilters = React.useCallback(() => {
if (!column) return;
column.setFilterValue(undefined);
}, [column]);
if (!column) {
return null;
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className={cn("h-8 gap-2 px-3", selectedCount > 0 && "bg-primary/5")}
>
<ListFilterIcon className="size-4" />
<span className="text-sm font-medium">فیلتر</span>
{selectedCount > 0 && (
<Badge
variant="secondary"
className="rounded-sm px-1 text-[11px] font-normal"
>
{formatNumber(selectedCount)}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent
align={align}
className="w-auto min-w-36 space-y-3"
sideOffset={8}
>
<div className="space-y-3">
<p className="text-xs font-semibold text-muted-foreground/70">
{title}
</p>
<div className="space-y-2">
{options.map((option, index) => {
const checkboxId = `${column.id ?? "filter"}-${index}`;
const isSelected = selectedValues.has(option.value.toLowerCase());
const OptionIcon = option.icon;
const count =
facetCounts?.get(option.value) ??
facetCounts?.get(option.value.toLowerCase());
return (
<div
key={option.value}
className={cn(
"flex items-center gap-3 rounded-lg px-2 py-1.5 transition-colors",
isSelected ? "bg-muted" : "hover:bg-muted/70",
)}
>
<Checkbox
id={checkboxId}
checked={isSelected}
onCheckedChange={(checked) =>
handleValueChange(option.value, Boolean(checked))
}
aria-label={`انتخاب ${option.label}`}
/>
<Label
htmlFor={checkboxId}
className="flex w-full items-center justify-between gap-3 text-sm"
>
<span className="flex items-center gap-2">
{OptionIcon && (
<OptionIcon className="size-3.5 text-muted-foreground" />
)}
{option.label}
</span>
{typeof count === "number" && !Number.isNaN(count) && (
<span className="text-xs text-muted-foreground">
{formatNumber(count)}
</span>
)}
</Label>
</div>
);
})}
</div>
</div>
<Button
variant="outline"
size="sm"
type="button"
className={cn(
"h-8 w-full justify-center rounded-md border-dashed bg-muted/40 text-xs font-semibold tracking-wide text-muted-foreground",
selectedCount === 0 &&
"border-muted/40 bg-muted/10 text-muted-foreground/70",
)}
onClick={clearFilters}
disabled={selectedCount === 0}
>
پاک کردن فیلترها
</Button>
</PopoverContent>
</Popover>
);
}
interface DataTableTabbarProps<TData> extends React.ComponentProps<"div"> {
_table: TableType<TData>;
search?: React.ReactNode;
filter?: React.ReactNode;
}
export function DataTableTabbar<TData>({
_table,
children,
search,
filter,
className,
...props
}: DataTableTabbarProps<TData>) {
return (
<div
className={cn("flex items-center justify-between py-4", className)}
{...props}
>
<div className="flex flex-1 items-center gap-2">{children}</div>
<div className="flex w-full items-center gap-2 sm:w-auto">
{search}
{filter}
</div>
</div>
);
}
export const schema = z.object({
id: z.string(),
purchased: z.string(),
order_status: z.string(),
customer: z.string(),
payment_method: z.string(),
payment_status: z.string(),
items: z.number(),
});
const data = [
{
id: "#301456",
purchased: "کاپشن چرمی",
order_status: "آماده تحویل",
customer: "سارا احمدی",
payment_method: "****7321",
payment_status: "پرداخت‌شده",
items: 3,
},
{
id: "#789123",
purchased: "کفش اسپرت آبی",
order_status: "تحویل‌شده",
customer: "رضا محمدی",
payment_method: "****4509",
payment_status: "پرداخت‌شده",
items: 2,
},
{
id: "#654321",
purchased: "هودی قرمز",
order_status: "تحویل‌شده",
customer: "نیلوفر حسینی",
payment_method: "****9812",
payment_status: "در انتظار پیگیری",
items: 4,
},
{
id: "#987654",
purchased: "عینک آفتابی مشکی",
order_status: "تحویل‌نشده",
customer: "داوود کریمی",
payment_method: "****6734",
payment_status: "در انتظار پیگیری",
items: 1,
},
{
id: "#432187",
purchased: "کوله‌پشتی سبز",
order_status: "تحویل‌شده",
customer: "لیلا رحیمی",
payment_method: "****2901",
payment_status: "پرداخت‌شده",
items: 2,
},
{
id: "#567890",
purchased: "ساعت نقره‌ای",
order_status: "آماده تحویل",
customer: "امیر نوری",
payment_method: "****8456",
payment_status: "در انتظار پیگیری",
items: 1,
},
{
id: "#234567",
purchased: "شال خاکستری",
order_status: "تحویل‌شده",
customer: "راحله موسوی",
payment_method: "****1234",
payment_status: "مسترد شده",
items: 3,
},
{
id: "#876543",
purchased: "پیراهن بنفش",
order_status: "تحویل‌نشده",
customer: "توماس علوی",
payment_method: "****5678",
payment_status: "مسترد جزئی",
items: 2,
},
{
id: "#345678",
purchased: "کلاه سفید",
order_status: "آماده تحویل",
customer: "آنا رحیمی",
payment_method: "****9012",
payment_status: "پرداخت‌شده",
items: 1,
},
{
id: "#456789",
purchased: "بوت قهوه‌ای",
order_status: "تحویل‌شده",
customer: "حامد سنگری",
payment_method: "****3456",
payment_status: "پرداخت‌شده",
items: 4,
},
{
id: "#678901",
purchased: "کیف دوشی زرد",
order_status: "تحویل‌شده",
customer: "سوفیا تهرانی",
payment_method: "****7890",
payment_status: "در انتظار پیگیری",
items: 2,
},
{
id: "#890123",
purchased: "دستکش نارنجی",
order_status: "تحویل‌نشده",
customer: "هانیه کلارک",
payment_method: "****2345",
payment_status: "در انتظار پیگیری",
items: 1,
},
{
id: "#123456",
purchased: "پلیور صورتی",
order_status: "آماده تحویل",
customer: "گلناز ایوانس",
payment_method: "****6789",
payment_status: "پرداخت‌شده",
items: 3,
},
{
id: "#543210",
purchased: "کمربند مشکی",
order_status: "تحویل‌شده",
customer: "دانیال میرزایی",
payment_method: "****1122",
payment_status: "مسترد شده",
items: 1,
},
{
id: "#765432",
purchased: "کاپشن فیروزه‌ای",
order_status: "تحویل‌نشده",
customer: "الهام سفید",
payment_method: "****5566",
payment_status: "مسترد جزئی",
items: 2,
},
{
id: "#987012",
purchased: "شلوارک سرمه‌ای",
order_status: "آماده تحویل",
customer: "ایمان خاکپور",
payment_method: "****9900",
payment_status: "در انتظار پیگیری",
items: 5,
},
{
id: "#210987",
purchased: "گردنبند طلا",
order_status: "تحویل‌شده",
customer: "مریم اسکات",
payment_method: "****3344",
payment_status: "پرداخت‌شده",
items: 1,
},
{
id: "#432109",
purchased: "شلوار زیتونی",
order_status: "تحویل‌شده",
customer: "یاسین تپل",
payment_method: "****7788",
payment_status: "پرداخت‌شده",
items: 3,
},
{
id: "#6543210",
purchased: "کلاه قرمز",
order_status: "تحویل‌نشده",
customer: "کلوئه یونگ",
payment_method: "****1122",
payment_status: "در انتظار پیگیری",
items: 1,
},
{
id: "#8765432",
purchased: "جین آبی",
order_status: "آماده تحویل",
customer: "نوح لوئیس",
payment_method: "****5566",
payment_status: "پرداخت‌شده",
items: 2,
},
{
id: "#109876",
purchased: "پیراهن سبز",
order_status: "تحویل‌شده",
customer: "آوا شاهی",
payment_method: "****9900",
payment_status: "مسترد شده",
items: 4,
},
{
id: "#321098",
purchased: "دامن بنفش",
order_status: "تحویل‌نشده",
customer: "لیام آدامز",
payment_method: "****3344",
payment_status: "مسترد جزئی",
items: 1,
},
{
id: "#5432109",
purchased: "کیف پول مشکی",
order_status: "آماده تحویل",
customer: "ایزابلا بکر",
payment_method: "****7788",
payment_status: "در انتظار پیگیری",
items: 2,
},
{
id: "#7654321",
purchased: "شال زرد",
order_status: "تحویل‌شده",
customer: "مازیار سبز",
payment_method: "****1122",
payment_status: "پرداخت‌شده",
items: 1,
},
{
id: "#9876543",
purchased: "کفش سفید",
order_status: "تحویل‌شده",
customer: "الا مارتینز",
payment_method: "****5566",
payment_status: "پرداخت‌شده",
items: 3,
},
];
export type StatusOption = {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
};
export const productTypes = [
{
value: "electronics",
label: "الکترونیک",
icon: Smartphone,
},
{
value: "accessories",
label: "لوازم جانبی",
icon: ShoppingBag,
},
{
value: "clothing",
label: "پوشاک",
icon: Shirt,
},
{
value: "footwear",
label: "کفش",
icon: Footprints,
},
{
value: "shoes",
label: "کفش ورزشی",
icon: PackageIcon,
},
];
export const orderStatuses: StatusOption[] = [
{ label: "تحویل‌شده", value: "تحویل‌شده", icon: CheckCircle },
{ label: "آماده تحویل", value: "آماده تحویل", icon: Package },
{ label: "تحویل‌نشده", value: "تحویل‌نشده", icon: Clock },
];
export const paymentStatuses: StatusOption[] = [
{ label: "پرداخت‌شده", value: "پرداخت‌شده", icon: CheckCheckIcon },
{ label: "در انتظار پیگیری", value: "در انتظار پیگیری", icon: LoaderIcon },
{ label: "مسترد شده", value: "مسترد شده", icon: RefreshCcw },
{
label: "مسترد جزئی",
value: "مسترد جزئی",
icon: PieChart,
},
];
const createStatusLookup = (options: StatusOption[]) =>
options.reduce<Record<string, StatusOption>>((acc, option) => {
acc[option.value] = option;
return acc;
}, {});
const orderStatusLookup = createStatusLookup(orderStatuses);
const paymentStatusLookup = createStatusLookup(paymentStatuses);
export const getOrderStatusMeta = (status: string) => orderStatusLookup[status];
export const getPaymentStatusMeta = (status: string) =>
paymentStatusLookup[status];
export const getStatusVariant = (status: string) => {
switch (status) {
case "تحویل‌شده":
return "default";
case "آماده تحویل":
return "secondary";
case "تحویل‌نشده":
return "outline";
default:
return "outline";
}
};
export const getPaymentVariant = (status: string) => {
switch (status) {
case "پرداخت‌شده":
return "default";
case "در انتظار پیگیری":
return "secondary";
case "مسترد شده":
return "outline";
case "مسترد جزئی":
return "outline";
default:
return "outline";
}
};
type OrderStatusTab = "all" | string;
export const validatedData = schema.array().parse(data);
export const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
accessorKey: "order",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="سفارش" />
),
cell: ({ row }) => (
<div className="w-[100px] font-mono">{row.original.id}</div>
),
enableHiding: false,
meta: {
label: "سفارش",
},
},
{
accessorKey: "purchased",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="خریداری‌شده" />
),
cell: ({ row }) => {
const items = row.original.items;
return (
<div className="flex gap-2">
<div className="max-w-[500px] truncate font-medium">
{row.original.purchased}
</div>
{items && (
<Badge variant="outline">{formatNumber(items)}</Badge>
)}
</div>
);
},
enableSorting: false,
enableHiding: false,
meta: {
label: "خریداری‌شده",
},
},
{
accessorKey: "customer",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="مشتری" />
),
cell: ({ row }) => (
<div className="max-w-[150px] truncate">{row.original.customer}</div>
),
meta: {
label: "مشتری",
},
},
{
accessorKey: "order_status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="وضعیت معامله سفارش" />
),
cell: ({ row }) => {
const status = row.original.order_status;
const statusMeta = getOrderStatusMeta(status);
const StatusIcon = statusMeta?.icon;
return (
<div className="flex items-center gap-2">
<Badge
variant={getStatusVariant(status)}
className="gap-1 whitespace-nowrap"
>
{StatusIcon && <StatusIcon className="size-3" />}
<span>{statusMeta?.label ?? status}</span>
</Badge>
</div>
);
},
filterFn: (row, id, value) => {
if (!Array.isArray(value) || value.length === 0) {
return true;
}
const rowValue = String(row.getValue(id)).toLowerCase();
return value
.map(String)
.some((entry) => entry.toLowerCase() === rowValue);
},
enableSorting: false,
meta: {
label: "وضعیت معامله سفارش",
},
},
{
accessorKey: "payment_status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="وضعیت معامله پرداخت" />
),
cell: ({ row }) => {
const status = row.original.payment_status;
const statusMeta = getPaymentStatusMeta(status);
const StatusIcon = statusMeta?.icon;
return (
<div className="flex items-center gap-2">
<Badge
variant={getPaymentVariant(status)}
className="gap-1 whitespace-nowrap"
>
{StatusIcon && <StatusIcon className="size-3" />}
<span>{statusMeta?.label ?? status}</span>
</Badge>
</div>
);
},
filterFn: (row, id, value) => {
if (!Array.isArray(value) || value.length === 0) {
return true;
}
const rowValue = String(row.getValue(id)).toLowerCase();
return value
.map(String)
.some((entry) => entry.toLowerCase() === rowValue);
},
enableSorting: false,
meta: {
label: "وضعیت معامله پرداخت",
},
},
{
accessorKey: "payment_method",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="روش پرداخت" />
),
cell: ({ row }) => (
<div className="font-mono text-sm">{row.original.payment_method}</div>
),
enableSorting: false,
meta: {
label: "روش پرداخت",
},
},
];
export const DataTable14 = ({ className }: { className?: string }) => {
const { table, setColumnFilters } = useDataTable({
data: validatedData,
columns,
getRowId: (row) => row.id.toString(),
});
const orderStatusColumn = table.getColumn("order_status");
const paymentStatusColumn = table.getColumn("payment_status");
const [orderStatusTab, setOrderStatusTab] =
React.useState<OrderStatusTab>("all");
const orderStatusCounts = React.useMemo(
() => orderStatusColumn?.getFacetedUniqueValues(),
[orderStatusColumn],
);
const allOrdersCount = React.useMemo(() => {
if (!orderStatusCounts?.size) {
return table.getPreFilteredRowModel().rows.length;
}
return Array.from(orderStatusCounts.values()).reduce(
(acc, count) => acc + Number(count),
0,
);
}, [orderStatusCounts, table]);
// Single unified handler for status tab changes
const handleStatusTabChange = React.useCallback(
(value: string) => {
setOrderStatusTab(value);
setColumnFilters((prev) => {
const filtered = prev.filter((f) => f.id !== "order_status");
return value === "all"
? filtered
: [...filtered, { id: "order_status", value: [value] }];
});
},
[setColumnFilters],
);
const PaymentStatusFilter = paymentStatusColumn ? (
<DataTableButtonFilter
column={paymentStatusColumn}
options={paymentStatuses}
title="وضعیت معامله پرداخت"
align="start"
/>
) : null;
const Search = (
<Input
placeholder="جستجو در همه ستون‌ها..."
value={(table.getState().globalFilter as string) ?? ""}
onChange={(event) => table.setGlobalFilter(event.target.value)}
className="h-8 w-full lg:w-[250px]"
/>
);
const Toolbar = (
<DataTableTabbar
_table={table}
search={Search}
filter={PaymentStatusFilter}
className="flex-wrap gap-y-2"
>
{orderStatusColumn && (
<Tabs
value={orderStatusTab}
onValueChange={handleStatusTabChange}
className="w-full min-w-[260px] sm:w-auto"
>
<TabsList
className="flex w-full flex-wrap justify-start gap-1 group-data-horizontal/tabs:h-auto"
role="tablist"
aria-label="فیلتر سفارش‌ها بر اساس وضعیت معامله"
>
<TabsTrigger value="all" className="gap-2">
<span>همه</span>
<Badge variant="secondary" className="rounded-sm px-1.5">
{formatNumber(allOrdersCount)}
</Badge>
</TabsTrigger>
{orderStatuses.map((status) => {
const StatusIcon = status.icon;
const count = orderStatusCounts?.get(status.value) ?? 0;
return (
<TabsTrigger
key={status.value}
value={status.value}
className="gap-2"
>
{StatusIcon && <StatusIcon className="size-3" />}
<span className="hidden text-xs md:inline">
{status.label}
</span>
<Badge variant="secondary" className="rounded-sm px-1.5">
{formatNumber(count)}
</Badge>
</TabsTrigger>
);
})}
</TabsList>
</Tabs>
)}
</DataTableTabbar>
);
return (
<section dir="rtl" lang="fa" className={cn("w-full py-32 lg:w-auto", className)}>
<div className="container">
<div className="w-full">
<div className="mb-8 text-start">
<h2 className="text-xl font-bold tracking-tight">
جدول با فیلتر تب‌دار و دکمه فیلتر
</h2>
<p className="mt-2 text-muted-foreground">
جدول سفارشات با مرتب‌سازی، جستجوی سراسری، فیلتر وضعیت معامله از طریق
تب‌ها، فیلتر وضعیت معامله پرداخت با دکمه و کنترل‌های کامل صفحه‌بندی.
</p>
</div>
{Toolbar}
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{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>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
نتیجه‌ای یافت نشد.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
</div>
</section>
);
};