"use client"; import type { ColumnFiltersState, RowSelectionState, VisibilityState, } from "@tanstack/react-table"; import { type Column, type ColumnDef, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type SortingState, type Table as TableType, useReactTable, } from "@tanstack/react-table"; import { AnimatePresence, motion } from "framer-motion"; import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ChevronsUpDown, Download, Settings2, Trash2, X, } from "lucide-react"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { toast } from "sonner"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; const formatPrice = (value: number) => new Intl.NumberFormat("fa-IR", { style: "currency", currency: "IRR", }).format(value); const formatNumber = (value: number) => value.toLocaleString("fa-IR"); type UseDataTableOptions = { data: Array; columns: Array>; getRowId?: (row: TData) => string; initialSorting?: SortingState; initialGlobalFilter?: string; initialFilters?: ColumnFiltersState; initialVisibility?: VisibilityState; initialSelection?: RowSelectionState; enableRowSelection?: boolean; }; export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialFilters = [], initialGlobalFilter = "", initialVisibility = {}, initialSelection = {}, enableRowSelection = true, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [columnFilters, setColumnFilters] = React.useState(initialFilters); const [globalFilter, setGlobalFilter] = React.useState(initialGlobalFilter); const [columnVisibility, setColumnVisibility] = React.useState(initialVisibility); const [rowSelection, setRowSelection] = React.useState(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, enableRowSelection, }); return { table, columnFilters, setColumnFilters, sorting, setSorting, globalFilter, setGlobalFilter, columnVisibility, setColumnVisibility, rowSelection, setRowSelection, }; } type DataTableColumnHeaderProps = { column: Column; title: string; }; export const DataTableColumnHeader = ({ column, title, }: DataTableColumnHeaderProps) => { const canSort = column.getCanSort(); const sorted = column.getIsSorted(); if (!canSort) { return ( {title} ); } return ( ); }; type DataTablePaginationProps = { table: TableType; pageSizeOptions?: number[]; }; export const DataTablePagination = ({ table, pageSizeOptions = [10, 20, 30, 40, 50], }: DataTablePaginationProps) => { 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 (
{totalRows === 0 ? ( "نتیجه‌ای یافت نشد" ) : ( <> نمایش {formatNumber(startRow)} تا {formatNumber(endRow)} از{" "} {formatNumber(totalRows)} رکورد )}

تعداد در هر صفحه

صفحه {formatNumber(currentPage + 1)} از {formatNumber(pageCount)}
); }; interface DataTableViewOptionsProps { table: TableType; } export function DataTableViewOptions({ table, }: DataTableViewOptionsProps) { const columns = table.getAllColumns().filter((column) => column.getCanHide()); const hiddenCount = columns.filter((col) => !col.getIsVisible()).length; const hasHiddenColumns = hiddenCount > 0; const handleResetColumns = () => { table.resetColumnVisibility(); }; return ( نمایش ستون‌ها {hasHiddenColumns && ( )} {columns.length === 0 ? (
ستونی در دسترس نیست
) : ( columns.map((column) => { const label = (column.columnDef.meta as { label?: string })?.label || column.id; return ( column.toggleVisibility(!!value)} > {label} ); }) )}
); } interface DataTableActionBarProps extends React.ComponentProps< typeof motion.div > { table: TableType; visible?: boolean; container?: Element | DocumentFragment | null; position?: "top" | "bottom"; // Add this prop } function DataTableActionBar({ table, visible: visibleProp, container: containerProp, position = "bottom", children, className, ...props }: DataTableActionBarProps) { const [mounted, setMounted] = React.useState(false); React.useLayoutEffect(() => { setMounted(true); }, []); React.useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { table.toggleAllRowsSelected(false); } } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [table]); const container = containerProp ?? (mounted ? globalThis.document?.body : null); if (!container) return null; const visible = visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0; const variants = { top: { position: "top-12", animation: { initial: { opacity: 0, y: -20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, }, extraClasses: "shadow-sm", }, bottom: { position: "bottom-6", animation: { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 20 }, }, extraClasses: "shadow-sm", }, }; const currentVariant = variants[position]; return ReactDOM.createPortal( {visible && ( {children} )} , container, ); } interface DataTableActionBarActionProps extends React.ComponentProps< typeof Button > { tooltip?: string; isPending?: boolean; } function DataTableActionBarAction({ size = "sm", tooltip, isPending, disabled, className, children, ...props }: DataTableActionBarActionProps) { const trigger = ( ); if (!tooltip) return trigger; return ( {trigger}

{tooltip}

); } interface DataTableActionBarSelectionProps { table: TableType; } function DataTableActionBarSelection({ table, }: DataTableActionBarSelectionProps) { const onClearSelection = React.useCallback(() => { table.toggleAllRowsSelected(false); }, [table]); return (
{formatNumber(table.getFilteredSelectedRowModel().rows.length)} مورد انتخاب شده

پاک کردن انتخاب

Esc
); } interface DataTableToolbarProps extends React.ComponentProps<"div"> { table: TableType; showViewOptions?: boolean; } export const DataTableToolbar = ({ table, children, className, showViewOptions = true, ...props }: DataTableToolbarProps) => { return (
{children}
{showViewOptions && }
); }; export const schema = z.object({ id: z.string(), item: z.string(), type: z.string(), stock: z.boolean(), sku: z.string(), price: z.number(), availability: z.array(z.enum(["فروشگاه", "آنلاین"])), }); const data = [ { id: "prod-001", item: "قاب تبلت", type: "الکترونیک", stock: true, sku: "TC-001", price: 83.24, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-002", item: "ساعت هوشمند", type: "الکترونیک", stock: true, sku: "SW-002", price: 246.27, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-003", item: "پلیور پشمی", type: "لوازم جانبی", stock: true, sku: "WS-003", price: 168.27, availability: ["فروشگاه"], }, { id: "prod-004", item: "هدفون بی‌سیم", type: "الکترونیک", stock: true, sku: "WE-004", price: 107.75, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-005", item: "کاور لپ‌تاپ", type: "الکترونیک", stock: true, sku: "LS-005", price: 248.02, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-006", item: "کفش دویدن", type: "کفش", stock: true, sku: "RS-006", price: 208.26, availability: ["فروشگاه"], }, { id: "prod-007", item: "کاپشن زمستانی", type: "پوشاک", stock: true, sku: "WJ-007", price: 148.06, availability: ["فروشگاه"], }, { id: "prod-008", item: "قاب گوشی", type: "لوازم جانبی", stock: true, sku: "PC-008", price: 298.08, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-009", item: "ردیاب تناسب اندام", type: "پوشاک", stock: true, sku: "FT-009", price: 222.09, availability: ["فروشگاه"], }, { id: "prod-010", item: "عینک آفتابی", type: "لوازم جانبی", stock: true, sku: "SG-010", price: 60.17, availability: ["فروشگاه"], }, { id: "prod-011", item: "بوت روزمره", type: "کفش", stock: true, sku: "CB-011", price: 276.15, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-012", item: "کفش پاشنه‌بلند", type: "پوشاک", stock: true, sku: "HH-012", price: 85.47, availability: ["فروشگاه"], }, { id: "prod-013", item: "صندل ساحلی", type: "کفش", stock: true, sku: "BS-013", price: 41.25, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-014", item: "گردنبند طلا", type: "لوازم جانبی", stock: true, sku: "GN-014", price: 201.47, availability: ["فروشگاه", "آنلاین"], }, { id: "prod-015", item: "اسپیکر بلوتوث", type: "الکترونیک", stock: true, sku: "BS-015", price: 297.37, availability: ["فروشگاه", "آنلاین"], }, ]; export const validatedData = schema.array().parse(data); export const columns: ColumnDef>[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="انتخاب همه" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="انتخاب رکورد" /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "sku", header: ({ column }) => ( ), enableSorting: true, enableHiding: true, meta: { label: "کد کالا", }, }, { accessorKey: "item", header: ({ column }) => ( ), cell: ({ row }) => (
{row.getValue("item")}
), enableSorting: true, enableHiding: false, meta: { label: "نام مخاطب محصول", }, }, { accessorKey: "type", header: ({ column }) => ( ), enableSorting: true, enableHiding: true, meta: { label: "دسته‌بندی", }, }, { accessorKey: "stock", header: ({ column }) => ( ), cell: ({ row }) => { const inStock: boolean = row.getValue("stock"); return inStock ? "بله" : "خیر"; }, enableSorting: false, enableHiding: true, meta: { label: "وضعیت معامله موجودی", }, }, { accessorKey: "price", header: ({ column }) => ( ), cell: ({ row }) => { const price = parseFloat(row.getValue("price")); return
{formatPrice(price)}
; }, enableSorting: true, enableHiding: true, meta: { label: "قیمت", }, }, { accessorKey: "availability", header: ({ column }) => ( ), cell: ({ row }) => { const availability: ("فروشگاه" | "آنلاین")[] = row.getValue("availability"); return (
{availability.map((location) => ( {location} ))}
); }, enableSorting: false, enableHiding: true, meta: { label: "دسترس‌پذیری", }, }, ]; const _actions = ["export", "delete"] as const; type Action = (typeof _actions)[number]; interface DataTable11ActionBarProps { table: TableType>; } export function DataTable11ActionBar({ table }: DataTable11ActionBarProps) { const rows = table.getFilteredSelectedRowModel().rows; const [isPending, startTransition] = React.useTransition(); const [currentAction, setCurrentAction] = React.useState(null); const visible = rows.length > 0; const getIsActionPending = React.useCallback( (action: Action) => isPending && currentAction === action, [isPending, currentAction], ); const onProductExport = React.useCallback(() => { setCurrentAction("export"); startTransition(async () => { // Simulate export action - replace with actual export logic await new Promise((resolve) => setTimeout(resolve, 1000)); toast.success("محصولات با موفقیت صادر شد"); table.toggleAllRowsSelected(false); }); }, [table]); const onProductDelete = React.useCallback(() => { setCurrentAction("delete"); startTransition(async () => { // Simulate delete action - replace with actual delete logic await new Promise((resolve) => setTimeout(resolve, 1000)); toast.success("محصولات با موفقیت حذف شد"); table.toggleAllRowsSelected(false); }); }, [table]); return (
); } export const DataTable11 = ({ className }: { className?: string }) => { const { table } = useDataTable({ data: validatedData, columns, getRowId: (row) => row.id.toString(), }); const hasSelectedRows = table.getFilteredSelectedRowModel().rows.length > 0; const Toolbar = ( table.setGlobalFilter(event.target.value)} className="h-8 w-full lg:w-[250px]" /> ); return (

جدول با انتخاب رکورد و نوار عملیات

جدول داده با مرتب‌سازی، جستجوی سراسری، مخفی‌سازی ستون‌ها، انتخاب چند رکورد و نوار عملیات شناور. شامل کنترل‌های کامل صفحه‌بندی با طراحی واکنش‌گرا.

{Toolbar}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( نتیجه‌ای یافت نشد. )}
{hasSelectedRows && }
); };