"use client"; import type { ColumnFiltersState, ColumnPinningState, Row, RowSelectionState, VisibilityState, } from "@tanstack/react-table"; import { type Column, type ColumnDef, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type SortingState, useReactTable, } from "@tanstack/react-table"; import { ArrowDown, ArrowUp, CheckCheckIcon, CheckCircle, ChevronsUpDown, Clock, LoaderIcon, MoreHorizontal, Package, PieChart, RefreshCcw, } 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 { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { cn } from "@/lib/utils"; const formatNumber = (value: number) => value.toLocaleString("fa-IR"); const DATA_URL = "https://raw.githubusercontent.com/zerostaticthemes/ری یو آی-library-data/refs/heads/main/data/orders-ecommerce-sample.json"; type UseDataTableOptions = { data: Array; columns: Array>; getRowId?: (row: TData) => string; initialSorting?: SortingState; initialGlobalFilter?: string; initialFilters?: ColumnFiltersState; initialVisibility?: VisibilityState; initialSelection?: RowSelectionState; initialColumnPinning?: ColumnPinningState; enableRowSelection?: boolean; }; type ColumnStyleMeta = { align?: "left" | "center" | "right"; headerClassName?: string; cellClassName?: string; className?: string; width?: number | string; minWidth?: number | string; maxWidth?: number | string; }; type ColumnWithPinning = { 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; }; function getColumnLayoutStyles( meta: ColumnStyleMeta, columnWithPinning: ColumnWithPinning, ) { const baseStyle: CSSProperties = {}; if (meta.width !== undefined) { baseStyle.width = typeof meta.width === "number" ? `${meta.width}px` : meta.width; } if (meta.minWidth !== undefined) { baseStyle.minWidth = typeof meta.minWidth === "number" ? `${meta.minWidth}px` : meta.minWidth; } if (meta.maxWidth !== undefined) { baseStyle.maxWidth = typeof meta.maxWidth === "number" ? `${meta.maxWidth}px` : meta.maxWidth; } if ( baseStyle.width === undefined && typeof columnWithPinning.getSize === "function" ) { const computedWidth = columnWithPinning.getSize(); if (typeof computedWidth === "number" && Number.isFinite(computedWidth)) { baseStyle.width = `${computedWidth}px`; } } const pinned = columnWithPinning.getIsPinned?.() ?? false; 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 resolvedWidth = baseStyle.width ?? (typeof columnWithPinning.getSize === "function" ? columnWithPinning.getSize() : undefined); if (resolvedWidth !== undefined) { pinnedStyle.width = typeof resolvedWidth === "number" ? `${resolvedWidth}px` : resolvedWidth; } } const alignClass = meta.align === "center" ? "text-center" : meta.align === "right" ? "text-end" : meta.align === "left" ? "text-start" : undefined; return { baseStyle, pinnedStyle, pinned, lastPinned, alignClass }; } export function useDataTable(options: UseDataTableOptions) { const { data, columns, getRowId, initialSorting = [], initialSelection = {}, enableRowSelection = true, initialColumnPinning = {}, } = options; const [sorting, setSorting] = React.useState(initialSorting); const [rowSelection, setRowSelection] = React.useState(initialSelection); const [columnPinning, setColumnPinning] = React.useState(initialColumnPinning); const table = useReactTable({ data, columns, getRowId, state: { sorting, columnPinning, rowSelection, }, onSortingChange: setSorting, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), onColumnPinningChange: setColumnPinning, enableRowSelection, }); return { table, sorting, setSorting, rowSelection, setRowSelection, columnPinning, setColumnPinning, }; } 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 ( ); }; interface DataTableRowActionsProps { row: Row; schema: z.ZodSchema; itemName: string; idField?: string; } export function DataTableRowActions({ row, schema, itemName, idField = "id", }: DataTableRowActionsProps) { const item = schema.parse(row.original) as TData; const handleCopy = async () => { const itemRecord = item as Record; const value = itemRecord[idField] ?? itemRecord.id; if (!value) { return; } if ( typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function" ) { return; } try { await navigator.clipboard.writeText(String(value)); } catch (error) { console.error("Could not copy to clipboard", error); } }; return ( عملیات کپی شناسه {itemName} ویرایش {itemName} مشاهده جزئیات ); } export const schema = z.object({ id: z.string(), purchased: z.string(), items: z.number(), customer: z.string(), customer_email: z.string(), customer_segment: z.string(), fulfillment_channel: z.string(), shipping_service: z.string(), warehouse: z.string(), order_status: z.string(), payment_status: z.string(), payment_method: z.string(), tracking_number: z.string(), assigned_to: z.string(), return_status: z.string(), coupon_code: z.string().nullable(), gift: z.boolean(), notes: z.string(), }); export type Order = z.infer; const orderStatusMap: Record = { Fulfilled: "تحویل‌شده", "Ready for pickup": "آماده تحویل", Unfulfilled: "تحویل‌نشده", }; const paymentStatusMap: Record = { Paid: "پرداخت‌شده", Pending: "در انتظار پیگیری", Refunded: "مسترد شده", "Partially refunded": "مسترد جزئی", }; const customerSegmentMap: Record = { Wholesale: "عمده‌فروشی", Marketplace: "بازارچه", VIP: "ویژه", Enterprise: "سازمانی", Retail: "خرده‌فروشی", }; const fulfillmentChannelMap: Record = { FBA: "FBA", "3PL": "شخص ثالث", Dropship: "دراپ‌شیپ", "Merchant Fulfilled": "ارسال فروشنده", }; const shippingServiceMap: Record = { "Expedited Air": "هوایی سریع", "Prime 1-Day": "تحویل یک‌روزه", "Prime 2-Day": "تحویل دو‌روزه", "Standard Ground": "ارسال عادی", "Same-Day Local": "تحویل همان‌روز", }; const returnStatusMap: Record = { Authorized: "تأیید‌شده", Requested: "درخواست‌شده", Received: "دریافت‌شده", None: "ندارد", }; const purchasedMap: Record = { "Leather Jacket": "کاپشن چرمی", "Blue Sneakers": "کفش اسپرت آبی", "Red Hoodie": "هودی قرمز", "Black Sunglasses": "عینک آفتابی مشکی", "Green Backpack": "کوله‌پشتی سبز", "Silver Watch": "ساعت نقره‌ای", "Gray Scarf": "شال خاکستری", "Purple Dress": "پیراهن بنفش", "White Hat": "کلاه سفید", "Brown Boots": "بوت قهوه‌ای", "Yellow Tote Bag": "کیف دوشی زرد", "Orange Gloves": "دستکش نارنجی", "Pink Sweater": "پلیور صورتی", "Black Belt": "کمربند مشکی", "Teal Jacket": "کاپشن فیروزه‌ای", "Navy Shorts": "شلوارک سرمه‌ای", "Gold Necklace": "گردنبند طلا", "Olive Pants": "شلوار زیتونی", "Red Cap": "کلاه قرمز", "Blue Jeans": "جین آبی", "Green Shirt": "پیراهن سبز", "Purple Skirt": "دامن بنفش", "Black Wallet": "کیف پول مشکی", "Yellow Scarf": "شال زرد", "White Sneakers": "کفش سفید", }; const customerMap: Record = { "#301456": "سارا احمدی", "#789123": "رضا محمدی", "#654321": "نیلوفر حسینی", "#987654": "داوود کریمی", "#432187": "لیلا رحیمی", "#567890": "امیر نوری", "#234567": "راحله موسوی", "#876543": "توماس علوی", "#345678": "آنا رحیمی", "#456789": "حامد سنگری", "#678901": "سوفیا تهرانی", "#890123": "هانیه کلارک", "#123456": "گلناز ایوانس", "#543210": "دانیال میرزایی", "#765432": "الهام سفید", "#987012": "ایمان خاکپور", "#210987": "مریم اسکات", "#432109": "یاسین تپل", "#6543210": "کلوئه یونگ", "#8765432": "نوح لوئیس", "#109876": "آوا شاهی", "#321098": "لیام آدامز", "#5432109": "ایزابلا بکر", "#7654321": "مازیار سبز", "#9876543": "الا مارتینز", }; const assignedToMap: Record = { "Morgan Blythe": "مژگان بیت", "Harper Singh": "هستی سینگ", "Riley Nguyen": "رایلی نگوین", "Taylor Chen": "تیلور چن", "Jordan Patel": "جردن پاتل", }; const mapValue = (map: Record, value: string) => map[value] ?? value; export const localizeOrder = (order: Order): Order => { const customer = customerMap[order.id] ?? order.customer; const shippingService = mapValue(shippingServiceMap, order.shipping_service); return { ...order, purchased: mapValue(purchasedMap, order.purchased), customer, customer_segment: mapValue(customerSegmentMap, order.customer_segment), fulfillment_channel: mapValue( fulfillmentChannelMap, order.fulfillment_channel, ), shipping_service: shippingService, order_status: mapValue(orderStatusMap, order.order_status), payment_status: mapValue(paymentStatusMap, order.payment_status), assigned_to: mapValue(assignedToMap, order.assigned_to), return_status: mapValue(returnStatusMap, order.return_status), notes: `${customer} ارسال ${shippingService} را ترجیح می‌دهد.`, }; }; export type StatusOption = { label: string; value: string; icon?: React.ComponentType<{ className?: string }>; }; 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>((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"; } }; export const columns: ColumnDef>[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="انتخاب همه" className="translate-y-[2px]" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="انتخاب رکورد" className="translate-y-[2px]" /> ), enableSorting: false, enableHiding: false, meta: { width: 56, minWidth: 48, headerClassName: "px-2", cellClassName: "px-2", align: "center", }, }, { accessorKey: "id", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.id}
), enableSorting: true, enableHiding: false, }, { accessorKey: "purchased", header: ({ column }) => ( ), cell: ({ row }) => { const items = row.original.items; return (
{row.original.purchased}
{items ? ( {formatNumber(items)} ) : null}
); }, enableSorting: false, enableHiding: false, }, { accessorKey: "customer", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.customer}
), }, { accessorKey: "customer_email", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.customer_email} ), }, { accessorKey: "customer_segment", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.customer_segment} ), meta: { className: "whitespace-nowrap" }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "fulfillment_channel", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.fulfillment_channel} ), meta: { className: "whitespace-nowrap" }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "shipping_service", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.shipping_service} ), }, { accessorKey: "warehouse", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.warehouse} ), meta: { className: "whitespace-nowrap" }, }, { accessorKey: "order_status", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.order_status; const statusMeta = getOrderStatusMeta(status); const StatusIcon = statusMeta?.icon; return (
{StatusIcon ? : null} {statusMeta?.label ?? status}
); }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.includes(rowValue); }, enableSorting: false, }, { accessorKey: "payment_status", header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.payment_status; const statusMeta = getPaymentStatusMeta(status); const StatusIcon = statusMeta?.icon; return (
{StatusIcon ? : null} {statusMeta?.label ?? status}
); }, filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) { return true; } const rowValue = String(row.getValue(id)); return value.includes(rowValue); }, enableSorting: false, }, { accessorKey: "payment_method", header: ({ column }) => ( ), cell: ({ row }) => (
{row.original.payment_method}
), }, { accessorKey: "items", header: ({ column }) => ( ), cell: ({ row }) => ( {formatNumber(row.original.items)} ), meta: { align: "center", className: "tabular-nums" }, }, { accessorKey: "tracking_number", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.tracking_number} ), meta: { className: "whitespace-nowrap" }, }, { accessorKey: "assigned_to", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.assigned_to} ), meta: { className: "whitespace-nowrap" }, }, { accessorKey: "return_status", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.return_status} ), filterFn: (row, id, value) => { if (!Array.isArray(value) || value.length === 0) return true; return value.includes(String(row.getValue(id))); }, }, { accessorKey: "coupon_code", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.coupon_code ?? ""} ), }, { accessorKey: "gift", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.gift ? "بله" : "خیر"} ), }, { accessorKey: "notes", header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.notes} ), }, { id: "actions", cell: ({ row }) => ( ), }, ]; export const DataTable15 = ({ className }: { className?: string }) => { const [data, setData] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let active = true; const fetchData = async () => { setError(null); setIsLoading(true); try { const res = await fetch(DATA_URL); if (!res.ok) { const message = `خطا در دریافت داده‌های سفارش (${res.status})`; console.error(message, res.statusText); if (active) setError(message); return; } const json = await res.json(); const parsed = schema.array().parse(json).map(localizeOrder); if (active) { setData(parsed); } } catch (error) { console.error("Error fetching orders data", error); if (active) setError("خطا در دریافت داده‌های سفارش"); } finally { if (active) setIsLoading(false); } }; fetchData(); return () => { active = false; }; }, []); const { table } = useDataTable({ data, columns, getRowId: (row) => row.id.toString(), initialColumnPinning: { right: ["select"], left: ["actions"] }, }); 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 (

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

جدول داده با ستون‌های انتخاب و عملیات ثابت هنگام اسکرول افقی.

{isLoading ? (
در حال بارگذاری سفارش‌ها…
) : null} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const meta = (header.column.columnDef.meta as ColumnStyleMeta) ?? {}; const columnWithPinning = header.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned, alignClass, } = getColumnLayoutStyles(meta, columnWithPinning); return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {error ? ( {error} ) : table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const meta = (cell.column.columnDef.meta as ColumnStyleMeta) ?? {}; const columnWithPinning = cell.column as unknown as ColumnWithPinning; const { baseStyle, pinnedStyle, pinned, lastPinned, alignClass, } = getColumnLayoutStyles(meta, columnWithPinning); return ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ); })} )) ) : ( نتیجه‌ای یافت نشد )}
); };