"use client"; import { type Column, type ColumnDef, type ColumnPinningState, flexRender, getCoreRowModel, getSortedRowModel, type SortingState, useReactTable, type VisibilityState, } from "@tanstack/react-table"; import { ArrowDown, ArrowUp, ChevronDown, ChevronLeft, ChevronRight, Columns3, EyeOff, Footprints, PackageIcon, Pin, PinOff, Shirt, ShoppingBag, Smartphone, } from "lucide-react"; import type { CSSProperties } from "react"; import * as React from "react"; import { z } from "zod"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { cn } from "@/lib/utils"; const formatPrice = (value: number) => new Intl.NumberFormat("fa-IR", { style: "currency", currency: "IRR", }).format(value); type UseDataTableOptions = { data: Array; columns: Array>; getRowId?: (row: TData) => string; initialSorting?: SortingState; initialColumnPinning?: ColumnPinningState; }; export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialColumnPinning = {}, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [columnOrder, setColumnOrder] = React.useState( columns.map((col) => col.id as string), ); const [columnPinning, setColumnPinning] = React.useState(initialColumnPinning); const [columnVisibility, setColumnVisibility] = React.useState({}); const table = useReactTable({ data, columns, getRowId, state: { sorting, columnOrder, columnPinning, columnVisibility, }, onSortingChange: setSorting, onColumnOrderChange: setColumnOrder, onColumnPinningChange: setColumnPinning, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); return { table, sorting, setSorting, columnOrder, setColumnOrder, columnPinning, setColumnPinning, columnVisibility, setColumnVisibility, }; } type DataTableColumnHeaderProps = { column: Column; title: string; columnId: string; columnOrder: string[]; allColumns: Column[]; onMoveColumn: (columnId: string, direction: "left" | "right") => void; onPinColumn: (columnId: string, position: "left" | "right" | false) => void; onHideColumn: (columnId: string) => void; }; export const DataTableColumnHeader = ({ column, title, columnId, columnOrder, allColumns, onMoveColumn, onPinColumn, onHideColumn, }: DataTableColumnHeaderProps) => { const canSort = column.getCanSort(); const sorted = column.getIsSorted(); const currentIndex = columnOrder.indexOf(columnId); const isFirst = currentIndex === 0; const isLast = currentIndex === columnOrder.length - 1; const isPinned = column.getIsPinned(); return (
{title} {canSort && ( <> column.toggleSorting(false)} className="flex items-center gap-2" > صعودی {sorted === "asc" && ( ✓ )} column.toggleSorting(true)} className="flex items-center gap-2" > نزولی {sorted === "desc" && ( ✓ )} )} onPinColumn(columnId, "left")} className="flex items-center gap-2" > ثابت در چپ {isPinned === "left" && ( ✓ )} onPinColumn(columnId, "right")} className="flex items-center gap-2" > ثابت در راست {isPinned === "right" && ( ✓ )} {isPinned && ( onPinColumn(columnId, false)} className="flex items-center gap-2" > لغو ثابت‌سازی )} onMoveColumn(columnId, "left")} disabled={isFirst} className="flex items-center gap-2" > انتقال به راست onMoveColumn(columnId, "right")} disabled={isLast} className="flex items-center gap-2" > انتقال به چپ ستون‌ها {allColumns .filter((col) => col.getCanHide()) .map((col) => { return ( col.toggleVisibility(!!value)} > {typeof col.columnDef.header === "string" ? col.columnDef.header : col.id} ); })} onHideColumn(columnId)} className="flex items-center gap-2" > مخفی کردن ستون
); }; 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(["فروشگاه", "آنلاین"])), }); type Product = z.infer; export 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: ["فروشگاه"], }, ]; export const validatedData = schema.array().parse(data); export const productTypes = [ { value: "الکترونیک", label: "الکترونیک", icon: Smartphone, }, { value: "لوازم جانبی", label: "لوازم جانبی", icon: ShoppingBag, }, { value: "پوشاک", label: "پوشاک", icon: Shirt, }, { value: "کفش", label: "کفش", icon: Footprints, }, { value: "کفش ورزشی", label: "کفش ورزشی", icon: PackageIcon, }, ]; export const columns: ColumnDef[] = [ { id: "sku", accessorKey: "sku", header: "کد کالا", cell: ({ row }) =>
{row.original.sku}
, enableSorting: true, enableHiding: false, }, { id: "item", accessorKey: "item", header: "محصول", cell: ({ row }) => (
{row.original.item}
), enableSorting: true, enableHiding: false, }, { id: "type", accessorKey: "type", header: "دسته‌بندی", cell: ({ row }) => { const productType = productTypes.find( (type) => type.value === row.original.type, ); if (!productType) { return null; } return (
{productType.icon && ( )} {productType.label}
); }, enableSorting: true, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.some((type) => String(type) === rowValue); }, }, { id: "stock", accessorKey: "stock", header: "موجود در انبار", cell: ({ row }) => { const inStock: boolean = row.getValue("stock"); return inStock ? "بله" : "خیر"; }, enableSorting: true, }, { id: "price", accessorKey: "price", header: "قیمت", cell: ({ row }) => { const price = parseFloat(row.getValue("price")); return
{formatPrice(price)}
; }, enableSorting: true, }, { id: "availability", accessorKey: "availability", header: "دسترس‌پذیر در", cell: ({ row }) => { const availability: ("فروشگاه" | "آنلاین")[] = row.getValue("availability"); return (
{availability.map((location) => ( {location} ))}
); }, enableSorting: false, }, ]; export function DataTable21({ className }: { className?: string }) { const { table, columnOrder, setColumnOrder } = useDataTable({ data: validatedData, columns, getRowId: (row) => row.id.toString(), }); const handleMoveColumn = React.useCallback( (columnId: string, direction: "left" | "right") => { setColumnOrder((currentOrder) => { const currentIndex = currentOrder.indexOf(columnId); if (currentIndex === -1) return currentOrder; const newIndex = direction === "left" ? currentIndex - 1 : currentIndex + 1; if (newIndex < 0 || newIndex >= currentOrder.length) { return currentOrder; } const newOrder = [...currentOrder]; const [removed] = newOrder.splice(currentIndex, 1); newOrder.splice(newIndex, 0, removed); return newOrder; }); }, [setColumnOrder], ); const handlePinColumn = React.useCallback( (columnId: string, position: "left" | "right" | false) => { table.getColumn(columnId)?.pin(position); }, [table], ); const handleHideColumn = React.useCallback( (columnId: string) => { table.getColumn(columnId)?.toggleVisibility(false); }, [table], ); const allColumns = table.getAllColumns(); const visibleColumnCount = Math.max(table.getVisibleLeafColumns().length, 1); const visibleColumnIds = table .getVisibleLeafColumns() .map((column) => column.id); const rightPinnedColumnIdSet = new Set( table.getState().columnPinning.right ?? [], ); const firstRightPinnedIndex = visibleColumnIds.findIndex((columnId) => rightPinnedColumnIdSet.has(columnId), ); const rightStickyDividerTargetId = firstRightPinnedIndex > 0 ? visibleColumnIds[firstRightPinnedIndex - 1] : null; return (

جدول با کنترل‌های ستون

جدول داده با کنترل کامل ستون‌ها شامل مرتب‌سازی، ثابت‌سازی، جابجایی و مدیریت نمایش.

{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const columnWithPinning = header.column as unknown as { getIsPinned?: () => "left" | "right" | false; getIsLastColumn?: ( position: "left" | "right", ) => boolean; getIsFirstColumn?: ( position: "left" | "right", ) => boolean; getStart?: (position: "left" | "right") => number; getAfter?: (position: "left" | "right") => number; getSize?: () => number; }; const pinned = columnWithPinning.getIsPinned?.(); const pinnedStyle: CSSProperties = {}; let lastPinned: "left" | "right" | undefined; if (pinned) { pinnedStyle.position = "sticky"; pinnedStyle.zIndex = 1; if (pinned === "left") { const start = columnWithPinning.getStart?.("left") ?? 0; pinnedStyle.left = `${start}px`; if (columnWithPinning.getIsLastColumn?.("left")) { lastPinned = "left"; } } if (pinned === "right") { const after = columnWithPinning.getAfter?.("right") ?? 0; pinnedStyle.right = `${after}px`; if (columnWithPinning.getIsFirstColumn?.("right")) { lastPinned = "right"; } } const pinnedWidth = columnWithPinning.getSize?.(); if ( typeof pinnedWidth === "number" && Number.isFinite(pinnedWidth) ) { pinnedStyle.width = pinnedWidth; } } return ( {header.isPlaceholder ? null : typeof header.column .columnDef.header === "string" ? ( ) : ( flexRender( header.column.columnDef.header, header.getContext(), ) )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const columnWithPinning = cell.column as unknown as { getIsPinned?: () => "left" | "right" | false; getIsLastColumn?: ( position: "left" | "right", ) => boolean; getIsFirstColumn?: ( position: "left" | "right", ) => boolean; getStart?: (position: "left" | "right") => number; getAfter?: (position: "left" | "right") => number; getSize?: () => number; }; const pinned = columnWithPinning.getIsPinned?.(); const pinnedStyle: CSSProperties = {}; let lastPinned: "left" | "right" | undefined; if (pinned) { pinnedStyle.position = "sticky"; pinnedStyle.zIndex = 1; if (pinned === "left") { const start = columnWithPinning.getStart?.("left") ?? 0; pinnedStyle.left = `${start}px`; if (columnWithPinning.getIsLastColumn?.("left")) { lastPinned = "left"; } } if (pinned === "right") { const after = columnWithPinning.getAfter?.("right") ?? 0; pinnedStyle.right = `${after}px`; if (columnWithPinning.getIsFirstColumn?.("right")) { lastPinned = "right"; } } const pinnedWidth = columnWithPinning.getSize?.(); if ( typeof pinnedWidth === "number" && Number.isFinite(pinnedWidth) ) { pinnedStyle.width = pinnedWidth; } } return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} )) ) : ( نتیجه‌ای یافت نشد )}
); }