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

1058 lines
32 KiB
TypeScript

"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<TData> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, unknown>>;
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<TData>(options: UseDataTableOptions<TData>) {
const {
data,
columns,
getRowId,
initialSorting = [],
initialSelection = {},
enableRowSelection = true,
initialColumnPinning = {},
} = options;
const [sorting, setSorting] = React.useState<SortingState>(initialSorting);
const [rowSelection, setRowSelection] =
React.useState<RowSelectionState>(initialSelection);
const [columnPinning, setColumnPinning] =
React.useState<ColumnPinningState>(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<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>
);
};
interface DataTableRowActionsProps<TData> {
row: Row<TData>;
schema: z.ZodSchema;
itemName: string;
idField?: string;
}
export function DataTableRowActions<TData>({
row,
schema,
itemName,
idField = "id",
}: DataTableRowActionsProps<TData>) {
const item = schema.parse(row.original) as TData;
const handleCopy = async () => {
const itemRecord = item as Record<string, unknown>;
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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-8 data-[state=open]:bg-muted"
>
<MoreHorizontal />
<span className="sr-only">باز کردن منو</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[160px]">
<DropdownMenuLabel>عملیات</DropdownMenuLabel>
<DropdownMenuItem onClick={handleCopy}>
کپی شناسه {itemName}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>ویرایش {itemName}</DropdownMenuItem>
<DropdownMenuItem>مشاهده جزئیات</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
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<typeof schema>;
const orderStatusMap: Record<string, string> = {
Fulfilled: "تحویل‌شده",
"Ready for pickup": "آماده تحویل",
Unfulfilled: "تحویل‌نشده",
};
const paymentStatusMap: Record<string, string> = {
Paid: "پرداخت‌شده",
Pending: "در انتظار پیگیری",
Refunded: "مسترد شده",
"Partially refunded": "مسترد جزئی",
};
const customerSegmentMap: Record<string, string> = {
Wholesale: "عمده‌فروشی",
Marketplace: "بازارچه",
VIP: "ویژه",
Enterprise: "سازمانی",
Retail: "خرده‌فروشی",
};
const fulfillmentChannelMap: Record<string, string> = {
FBA: "FBA",
"3PL": "شخص ثالث",
Dropship: "دراپ‌شیپ",
"Merchant Fulfilled": "ارسال فروشنده",
};
const shippingServiceMap: Record<string, string> = {
"Expedited Air": "هوایی سریع",
"Prime 1-Day": "تحویل یک‌روزه",
"Prime 2-Day": "تحویل دو‌روزه",
"Standard Ground": "ارسال عادی",
"Same-Day Local": "تحویل همان‌روز",
};
const returnStatusMap: Record<string, string> = {
Authorized: "تأیید‌شده",
Requested: "درخواست‌شده",
Received: "دریافت‌شده",
None: "ندارد",
};
const purchasedMap: Record<string, string> = {
"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<string, string> = {
"#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<string, string> = {
"Morgan Blythe": "مژگان بیت",
"Harper Singh": "هستی سینگ",
"Riley Nguyen": "رایلی نگوین",
"Taylor Chen": "تیلور چن",
"Jordan Patel": "جردن پاتل",
};
const mapValue = (map: Record<string, string>, 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<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";
}
};
export const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="انتخاب همه"
className="translate-y-[2px]"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => 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 }) => (
<DataTableColumnHeader column={column} title="سفارش" />
),
cell: ({ row }) => (
<div className="w-[120px] font-mono">{row.original.id}</div>
),
enableSorting: true,
enableHiding: false,
},
{
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-[320px] truncate font-medium">
{row.original.purchased}
</div>
{items ? (
<Badge variant="outline">{formatNumber(items)}</Badge>
) : null}
</div>
);
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "customer",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="مشتری" />
),
cell: ({ row }) => (
<div className="max-w-[180px] truncate">{row.original.customer}</div>
),
},
{
accessorKey: "customer_email",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="ایمیل مشتری" />
),
cell: ({ row }) => (
<span className="max-w-[240px] truncate text-xs sm:text-sm">
{row.original.customer_email}
</span>
),
},
{
accessorKey: "customer_segment",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="بخش" />
),
cell: ({ row }) => (
<Badge variant="outline">{row.original.customer_segment}</Badge>
),
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 }) => (
<DataTableColumnHeader column={column} title="کانال ارسال" />
),
cell: ({ row }) => (
<span className="text-xs whitespace-nowrap sm:text-sm">
{row.original.fulfillment_channel}
</span>
),
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 }) => (
<DataTableColumnHeader column={column} title="سرویس حمل" />
),
cell: ({ row }) => (
<span className="max-w-[240px] truncate text-xs sm:text-sm">
{row.original.shipping_service}
</span>
),
},
{
accessorKey: "warehouse",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="انبار" />
),
cell: ({ row }) => (
<span className="text-xs whitespace-nowrap sm:text-sm">
{row.original.warehouse}
</span>
),
meta: { className: "whitespace-nowrap" },
},
{
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" /> : null}
<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));
return value.includes(rowValue);
},
enableSorting: false,
},
{
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" /> : null}
<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));
return value.includes(rowValue);
},
enableSorting: false,
},
{
accessorKey: "payment_method",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="روش پرداخت" />
),
cell: ({ row }) => (
<div className="font-mono text-xs sm:text-sm">
{row.original.payment_method}
</div>
),
},
{
accessorKey: "items",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="تعداد" />
),
cell: ({ row }) => (
<span className="text-xs tabular-nums sm:text-sm">
{formatNumber(row.original.items)}
</span>
),
meta: { align: "center", className: "tabular-nums" },
},
{
accessorKey: "tracking_number",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="رهگیری" />
),
cell: ({ row }) => (
<span className="font-mono text-xs sm:text-sm">
{row.original.tracking_number}
</span>
),
meta: { className: "whitespace-nowrap" },
},
{
accessorKey: "assigned_to",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="مسئول" />
),
cell: ({ row }) => (
<span className="text-xs whitespace-nowrap sm:text-sm">
{row.original.assigned_to}
</span>
),
meta: { className: "whitespace-nowrap" },
},
{
accessorKey: "return_status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="وضعیت معامله مرجوعی" />
),
cell: ({ row }) => (
<Badge variant="outline">{row.original.return_status}</Badge>
),
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 }) => (
<DataTableColumnHeader column={column} title="کد تخفیف" />
),
cell: ({ row }) => (
<span className="text-xs sm:text-sm">
{row.original.coupon_code ?? ""}
</span>
),
},
{
accessorKey: "gift",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="هدیه" />
),
cell: ({ row }) => (
<Badge variant={row.original.gift ? "secondary" : "outline"}>
{row.original.gift ? "بله" : "خیر"}
</Badge>
),
},
{
accessorKey: "notes",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="یادداشت" />
),
cell: ({ row }) => (
<span className="max-w-[240px] truncate text-xs sm:text-sm">
{row.original.notes}
</span>
),
},
{
id: "actions",
cell: ({ row }) => (
<DataTableRowActions
row={row}
schema={schema}
itemName="سفارش"
idField="id"
/>
),
},
];
export const DataTable15 = ({ className }: { className?: string }) => {
const [data, setData] = React.useState<Order[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(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 (
<section dir="rtl" lang="fa" className={cn("py-32", 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>
<div className="overflow-hidden rounded-lg border-2">
<div className="relative">
{isLoading ? (
<div className="pointer-events-none absolute inset-0 z-30 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground">
<LoaderIcon className="h-4 w-4 animate-spin" />
<span>در حال بارگذاری سفارش‌ها…</span>
</div>
</div>
) : null}
<Table className="border-separate border-spacing-0 [&_td]:border-border [&_tfoot_td]:border-t [&_th]:border-b [&_th]:border-border [&_tr]:border-none [&_tr:hover_td[data-pinned]]:bg-muted/50 [&_tr:not(:last-child)_td]:border-b [&_tr[data-state=selected]_td[data-pinned]]:bg-muted [&_tr[data-state=selected]:hover_td[data-pinned]]:bg-muted">
<TableHeader className="bg-muted">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{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 (
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{ ...baseStyle, ...pinnedStyle }}
className={cn(
"relative h-11 truncate border-e px-3 text-sm font-medium last:border-e-0 data-[pinned]:bg-muted/90 data-[pinned]:backdrop-blur-xs [&[data-pinned=left][data-last-col=left]]:border-e [&[data-pinned=right][data-last-col=right]]:border-s",
header.column.id === rightStickyDividerTargetId &&
"!border-e-0",
meta.className,
meta.headerClassName,
alignClass,
)}
data-pinned={pinned || undefined}
data-last-col={lastPinned || undefined}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{error ? (
<TableRow className="border-b last:border-b-0">
<TableCell
colSpan={visibleColumnCount}
className="h-24 text-center text-sm text-destructive"
>
{error}
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="border-b last:border-b-0"
>
{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 (
<TableCell
key={cell.id}
style={{ ...baseStyle, ...pinnedStyle }}
className={cn(
"truncate border-e p-3 py-2 text-sm last:border-e-0 data-[pinned]:bg-background/90 data-[pinned]:backdrop-blur-xs [&[data-pinned=left][data-last-col=left]]:border-e [&[data-pinned=right][data-last-col=right]]:border-s",
cell.column.id === rightStickyDividerTargetId &&
"!border-e-0",
meta.className,
meta.cellClassName,
alignClass,
)}
data-pinned={pinned || undefined}
data-last-col={lastPinned || undefined}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
);
})}
</TableRow>
))
) : (
<TableRow className="border-b last:border-b-0">
<TableCell
colSpan={visibleColumnCount}
className="h-24 text-center text-sm text-muted-foreground"
>
نتیجه‌ای یافت نشد
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</div>
</section>
);
};