"use client"; import type { DragEndEvent } from "@dnd-kit/core"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"; import { arrayMove, horizontalListSortingStrategy, SortableContext, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import type { Cell, Column, ColumnDef, Header, RowSelectionState, SortingState, } from "@tanstack/react-table"; import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ChevronsUpDown, Footprints, GripVerticalIcon, PackageIcon, 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, 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; initialGlobalFilter?: string; initialSelection?: RowSelectionState; initialColumnOrder?: string[]; enableRowSelection?: boolean; }; export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialGlobalFilter = "", initialSelection = {}, initialColumnOrder = [], enableRowSelection = true, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [globalFilter, setGlobalFilter] = React.useState(initialGlobalFilter); const [rowSelection, setRowSelection] = React.useState(initialSelection); const [columnOrder, setColumnOrder] = React.useState(initialColumnOrder); const table = useReactTable({ data, columns, getRowId, state: { sorting, globalFilter, rowSelection, columnOrder, }, columnResizeMode: "onChange", onSortingChange: setSorting, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), onColumnOrderChange: setColumnOrder, enableRowSelection, }); return { table, sorting, setSorting, globalFilter, setGlobalFilter, rowSelection, setRowSelection, columnOrder, setColumnOrder, }; } type DataTableColumnHeaderProps = { column: Column; title: string; columnId: string; columnOrder: string[]; onMoveColumn: (columnId: string, direction: "left" | "right") => void; }; export const DataTableColumnHeader = ({ column, title, columnId, columnOrder, onMoveColumn, }: DataTableColumnHeaderProps) => { const canSort = column.getCanSort(); const sorted = column.getIsSorted(); const currentIndex = columnOrder.indexOf(columnId); const isFirst = currentIndex === 0; const isLast = currentIndex === columnOrder.length - 1; 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" && ✓} )} onMoveColumn(columnId, "left")} disabled={isFirst} className="flex items-center gap-2" > انتقال به راست onMoveColumn(columnId, "right")} disabled={isLast} 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, }, ]; const DraggableTableHeader = ({ header, interactive, columnOrder, onMoveColumn, }: { header: Header; interactive: boolean; columnOrder: string[]; onMoveColumn: (columnId: string, direction: "left" | "right") => void; }) => { const { attributes, isDragging, listeners, setNodeRef, transform, transition, } = useSortable({ id: header.column.id, }); const style: React.CSSProperties = { opacity: isDragging ? 0.7 : 1, position: "relative", transform: CSS.Translate.toString(transform), transition, whiteSpace: "nowrap", width: header.column.getSize(), zIndex: isDragging ? 1 : 0, }; return (
{interactive && ( )}
{header.isPlaceholder ? null : typeof header.column.columnDef .header === "string" ? ( ) : ( flexRender(header.column.columnDef.header, header.getContext()) )}
); }; const DraggableRowCell = ({ cell }: { cell: Cell }) => { const { isDragging, setNodeRef, transform, transition } = useSortable({ id: cell.column.id, }); const style: React.CSSProperties = { opacity: isDragging ? 0.7 : 1, position: "relative", transform: CSS.Translate.toString(transform), transition, width: cell.column.getSize(), zIndex: isDragging ? 1 : 0, }; return ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ); }; export function DataTable19({ className }: { className?: string }) { const [isMounted, setIsMounted] = React.useState(false); React.useEffect(() => { setIsMounted(true); }, []); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 5, }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const { table, setColumnOrder, columnOrder } = useDataTable({ data: validatedData, columns, getRowId: (row) => row.id.toString(), initialColumnOrder: columns.map((column) => column.id as string), }); const handleDragEnd = React.useCallback( ({ active, over }: DragEndEvent) => { if (!active || !over || active.id === over.id) { return; } setColumnOrder((columnOrder) => { const oldIndex = columnOrder.indexOf(active.id as string); const newIndex = columnOrder.indexOf(over.id as string); if (oldIndex === -1 || newIndex === -1) { return columnOrder; } return arrayMove(columnOrder, oldIndex, newIndex); }); }, [setColumnOrder], ); 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; // Bounds check if (newIndex < 0 || newIndex >= currentOrder.length) { return currentOrder; } return arrayMove(currentOrder, currentIndex, newIndex); }); }, [setColumnOrder], ); const rows = table.getRowModel().rows; return (

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

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

{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {rows?.length ? ( rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) ) : ( نتیجه‌ای یافت نشد )}
); }