Files

1610 lines
40 KiB
TypeScript

"use client";
import type {
Column,
ColumnDef,
PaginationState,
Row,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
ArrowDown,
ArrowUp,
CheckCircle,
ChevronsUpDown,
Clock,
LoaderIcon,
MoreHorizontal,
SquareMinusIcon,
SquarePlusIcon,
Truck,
XCircle,
} from "lucide-react";
import * as React from "react";
import { z } from "zod";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
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 formatPrice = (value: number) =>
new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
}).format(value);
type UseDataTableOptions<TData> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, unknown>>;
getRowId?: (row: TData) => string;
initialSorting?: SortingState;
initialGlobalFilter?: string;
initialSelection?: RowSelectionState;
enableRowSelection?: boolean;
};
export function useDataTable<TData>(options: UseDataTableOptions<TData>) {
const {
data,
columns,
getRowId,
initialSorting = [],
initialGlobalFilter = "",
initialSelection = {},
enableRowSelection = true,
} = options;
const [sorting, setSorting] = React.useState<SortingState>(initialSorting);
const [globalFilter, setGlobalFilter] =
React.useState<string>(initialGlobalFilter);
const [rowSelection, setRowSelection] =
React.useState<RowSelectionState>(initialSelection);
const table = useReactTable({
data,
columns,
getRowId,
state: {
sorting,
globalFilter,
rowSelection,
},
onSortingChange: setSorting,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
enableRowSelection,
});
return {
table,
sorting,
setSorting,
globalFilter,
setGlobalFilter,
rowSelection,
setRowSelection,
};
}
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 itemRecord = item as Record<string, unknown>;
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={() =>
navigator.clipboard.writeText(
String(itemRecord[idField] || itemRecord.id),
)
}
>
کپی شناسه {itemName}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>ویرایش {itemName}</DropdownMenuItem>
<DropdownMenuItem>مشاهده جزئیات</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
type StatusVariant = "info" | "primary" | "success" | "warning" | "destructive";
const OrderItemSchema = z.object({
id: z.string(),
productName: z.string(),
category: z.string(),
price: z.number(),
quantity: z.number(),
});
const OrderStatusSchema = z.object({
label: z.string(),
variant: z.enum(["info", "primary", "success", "warning", "destructive"]),
});
export const CustomerRecordSchema = z.object({
id: z.string(),
latestOrder: z.string(),
customer: z.object({
name: z.string(),
email: z.string(),
avatar: z.string(),
}),
total: z.number(),
totalItems: z.number(),
status: OrderStatusSchema,
items: z.array(OrderItemSchema),
});
type CustomerRecord = z.infer<typeof CustomerRecordSchema>;
type OrderItem = z.infer<typeof OrderItemSchema>;
type RawOrder = {
id: string;
orderNumber: string;
customer: string;
customerEmail: string;
customerAvatar: string;
total: string;
status: {
label: string;
variant: StatusVariant;
};
items: Array<{
id: string;
productName: string;
category: string;
price: string;
quantity: number;
}>;
};
const rawOrders: RawOrder[] = [
{
id: "1",
orderNumber: "ORD-2401",
customer: "Marcus Chen",
customerEmail: "marcus.chen@email.com",
customerAvatar: "1.png",
total: "$384.92",
status: {
label: "Processing",
variant: "info",
},
items: [
{
id: "1-1",
productName: "USB-C Hub",
category: "Electronics",
price: "$49.99",
quantity: 1,
},
{
id: "1-2",
productName: "Laptop Bag",
category: "Accessories",
price: "$79.99",
quantity: 1,
},
{
id: "1-3",
productName: "Portable SSD",
category: "Electronics",
price: "$129.99",
quantity: 1,
},
{
id: "1-4",
productName: "Cable Sleeve",
category: "Accessories",
price: "$14.99",
quantity: 2,
},
{
id: "1-5",
productName: "Webcam Cover",
category: "Accessories",
price: "$9.99",
quantity: 3,
},
{
id: "1-6",
productName: "Laptop Stand",
category: "Office",
price: "$54.99",
quantity: 1,
},
{
id: "1-7",
productName: "USB Cable Pack",
category: "Electronics",
price: "$19.99",
quantity: 1,
},
],
},
{
id: "2",
orderNumber: "ORD-2402",
customer: "Nina Patel",
customerEmail: "nina.patel@email.com",
customerAvatar: "2.png",
total: "$567.45",
status: {
label: "Shipped",
variant: "primary",
},
items: [
{
id: "2-1",
productName: "Air Purifier",
category: "Home",
price: "$189.99",
quantity: 1,
},
{
id: "2-2",
productName: "Essential Oil Set",
category: "Home",
price: "$44.99",
quantity: 1,
},
{
id: "2-3",
productName: "Humidifier",
category: "Home",
price: "$69.99",
quantity: 1,
},
{
id: "2-4",
productName: "Room Spray",
category: "Home",
price: "$24.99",
quantity: 2,
},
{
id: "2-5",
productName: "Aromatherapy Diffuser",
category: "Home",
price: "$39.99",
quantity: 1,
},
{
id: "2-6",
productName: "Scented Candles",
category: "Home",
price: "$34.99",
quantity: 3,
},
{
id: "2-7",
productName: "Pillow Spray",
category: "Home",
price: "$16.99",
quantity: 1,
},
{
id: "2-8",
productName: "Room Thermometer",
category: "Home",
price: "$19.99",
quantity: 1,
},
],
},
{
id: "3",
orderNumber: "ORD-2403",
customer: "Oliver Martinez",
customerEmail: "oliver.martinez@email.com",
customerAvatar: "3.png",
total: "$892.34",
status: {
label: "Delivered",
variant: "success",
},
items: [
{
id: "3-1",
productName: "Digital Camera",
category: "Electronics",
price: "$449.99",
quantity: 1,
},
{
id: "3-2",
productName: "Camera Bag",
category: "Accessories",
price: "$89.99",
quantity: 1,
},
{
id: "3-3",
productName: "Memory Card 128GB",
category: "Electronics",
price: "$34.99",
quantity: 2,
},
{
id: "3-4",
productName: "Tripod",
category: "Accessories",
price: "$79.99",
quantity: 1,
},
{
id: "3-5",
productName: "Camera Lens",
category: "Electronics",
price: "$199.99",
quantity: 1,
},
{
id: "3-6",
productName: "Lens Cleaning Kit",
category: "Accessories",
price: "$12.99",
quantity: 1,
},
{
id: "3-7",
productName: "Extra Battery",
category: "Electronics",
price: "$44.99",
quantity: 2,
},
{
id: "3-8",
productName: "Camera Strap",
category: "Accessories",
price: "$24.99",
quantity: 1,
},
{
id: "3-9",
productName: "Lens Filter",
category: "Accessories",
price: "$29.99",
quantity: 1,
},
{
id: "3-10",
productName: "Remote Shutter",
category: "Electronics",
price: "$19.99",
quantity: 1,
},
],
},
{
id: "4",
orderNumber: "ORD-2404",
customer: "Sophia Kim",
customerEmail: "sophia.kim@email.com",
customerAvatar: "4.png",
total: "$246.78",
status: {
label: "Pending",
variant: "warning",
},
items: [
{
id: "4-1",
productName: "Art Supplies Set",
category: "Art",
price: "$89.99",
quantity: 1,
},
{
id: "4-2",
productName: "Sketchbook",
category: "Art",
price: "$24.99",
quantity: 2,
},
{
id: "4-3",
productName: "Colored Pencils",
category: "Art",
price: "$39.99",
quantity: 1,
},
{
id: "4-4",
productName: "Watercolor Set",
category: "Art",
price: "$44.99",
quantity: 1,
},
{
id: "4-5",
productName: "Paint Brushes",
category: "Art",
price: "$29.99",
quantity: 1,
},
{
id: "4-6",
productName: "Canvas Pack",
category: "Art",
price: "$34.99",
quantity: 1,
},
],
},
{
id: "5",
orderNumber: "ORD-2405",
customer: "Jackson Wu",
customerEmail: "jackson.wu@email.com",
customerAvatar: "5.png",
total: "$1,234.56",
status: {
label: "Processing",
variant: "info",
},
items: [
{
id: "5-1",
productName: "Electric Bike",
category: "Sports",
price: "$899.99",
quantity: 1,
},
{
id: "5-2",
productName: "Bike Helmet",
category: "Sports",
price: "$79.99",
quantity: 1,
},
{
id: "5-3",
productName: "Bike Lock",
category: "Accessories",
price: "$49.99",
quantity: 1,
},
{
id: "5-4",
productName: "Bike Lights",
category: "Accessories",
price: "$39.99",
quantity: 1,
},
{
id: "5-5",
productName: "Water Bottle Holder",
category: "Accessories",
price: "$14.99",
quantity: 1,
},
{
id: "5-6",
productName: "Bike Phone Mount",
category: "Accessories",
price: "$24.99",
quantity: 1,
},
{
id: "5-7",
productName: "Repair Kit",
category: "Tools",
price: "$29.99",
quantity: 1,
},
{
id: "5-8",
productName: "Bike Pump",
category: "Tools",
price: "$34.99",
quantity: 1,
},
{
id: "5-9",
productName: "Bike Bell",
category: "Accessories",
price: "$9.99",
quantity: 1,
},
{
id: "5-10",
productName: "Reflective Vest",
category: "Clothing",
price: "$19.99",
quantity: 1,
},
{
id: "5-11",
productName: "Cycling Gloves",
category: "Clothing",
price: "$24.99",
quantity: 1,
},
],
},
{
id: "6",
orderNumber: "ORD-2406",
customer: "Emma Thompson",
customerEmail: "emma.thompson@email.com",
customerAvatar: "6.png",
total: "$678.90",
status: {
label: "Shipped",
variant: "primary",
},
items: [
{
id: "6-1",
productName: "Robot Vacuum",
category: "Home",
price: "$299.99",
quantity: 1,
},
{
id: "6-2",
productName: "Vacuum Filters",
category: "Home",
price: "$19.99",
quantity: 3,
},
{
id: "6-3",
productName: "Cleaning Solution",
category: "Home",
price: "$24.99",
quantity: 2,
},
{
id: "6-4",
productName: "Microfiber Cloths",
category: "Home",
price: "$14.99",
quantity: 2,
},
{
id: "6-5",
productName: "Dustpan Set",
category: "Home",
price: "$29.99",
quantity: 1,
},
{
id: "6-6",
productName: "Mop System",
category: "Home",
price: "$79.99",
quantity: 1,
},
{
id: "6-7",
productName: "Broom",
category: "Home",
price: "$34.99",
quantity: 1,
},
{
id: "6-8",
productName: "Trash Bags",
category: "Home",
price: "$19.99",
quantity: 2,
},
{
id: "6-9",
productName: "Gloves Pack",
category: "Home",
price: "$12.99",
quantity: 1,
},
{
id: "6-10",
productName: "Sponge Set",
category: "Home",
price: "$9.99",
quantity: 1,
},
{
id: "6-11",
productName: "Air Freshener",
category: "Home",
price: "$16.99",
quantity: 2,
},
{
id: "6-12",
productName: "Duster",
category: "Home",
price: "$19.99",
quantity: 1,
},
],
},
{
id: "7",
orderNumber: "ORD-2407",
customer: "Lucas Gonzalez",
customerEmail: "lucas.gonzalez@email.com",
customerAvatar: "7.png",
total: "$423.67",
status: {
label: "Delivered",
variant: "success",
},
items: [
{
id: "7-1",
productName: "Mechanical Keyboard",
category: "Electronics",
price: "$149.99",
quantity: 1,
},
{
id: "7-2",
productName: "Keycap Set",
category: "Accessories",
price: "$59.99",
quantity: 1,
},
{
id: "7-3",
productName: "Keyboard Wrist Rest",
category: "Accessories",
price: "$29.99",
quantity: 1,
},
{
id: "7-4",
productName: "Switch Puller",
category: "Tools",
price: "$9.99",
quantity: 1,
},
{
id: "7-5",
productName: "Stabilizers",
category: "Parts",
price: "$19.99",
quantity: 1,
},
{
id: "7-6",
productName: "Keyboard Cable",
category: "Accessories",
price: "$39.99",
quantity: 1,
},
{
id: "7-7",
productName: "Cleaning Brush",
category: "Tools",
price: "$7.99",
quantity: 1,
},
{
id: "7-8",
productName: "Lubricant",
category: "Tools",
price: "$14.99",
quantity: 1,
},
{
id: "7-9",
productName: "Extra Switches",
category: "Parts",
price: "$44.99",
quantity: 2,
},
],
},
{
id: "8",
orderNumber: "ORD-2408",
customer: "Ava Robinson",
customerEmail: "ava.robinson@email.com",
customerAvatar: "8.png",
total: "$789.23",
status: {
label: "Cancelled",
variant: "destructive",
},
items: [
{
id: "8-1",
productName: "Standing Desk",
category: "Furniture",
price: "$399.99",
quantity: 1,
},
{
id: "8-2",
productName: "Desk Converter",
category: "Furniture",
price: "$149.99",
quantity: 1,
},
{
id: "8-3",
productName: "Monitor Arm",
category: "Accessories",
price: "$119.99",
quantity: 1,
},
{
id: "8-4",
productName: "Cable Tray",
category: "Accessories",
price: "$24.99",
quantity: 1,
},
{
id: "8-5",
productName: "Desk Pad",
category: "Office",
price: "$29.99",
quantity: 1,
},
{
id: "8-6",
productName: "Footrest",
category: "Office",
price: "$39.99",
quantity: 1,
},
{
id: "8-7",
productName: "LED Desk Light",
category: "Lighting",
price: "$64.99",
quantity: 1,
},
],
},
{
id: "9",
orderNumber: "ORD-2409",
customer: "Ethan Foster",
customerEmail: "ethan.foster@email.com",
customerAvatar: "9.png",
total: "$534.12",
status: {
label: "Processing",
variant: "info",
},
items: [
{
id: "9-1",
productName: "Espresso Machine",
category: "Kitchen",
price: "$299.99",
quantity: 1,
},
{
id: "9-2",
productName: "Coffee Grinder",
category: "Kitchen",
price: "$89.99",
quantity: 1,
},
{
id: "9-3",
productName: "Milk Pitcher",
category: "Kitchen",
price: "$24.99",
quantity: 1,
},
{
id: "9-4",
productName: "Coffee Beans",
category: "Food",
price: "$29.99",
quantity: 2,
},
{
id: "9-5",
productName: "Tamper",
category: "Tools",
price: "$39.99",
quantity: 1,
},
{
id: "9-6",
productName: "Cleaning Tablets",
category: "Kitchen",
price: "$14.99",
quantity: 1,
},
{
id: "9-7",
productName: "Descaling Solution",
category: "Kitchen",
price: "$19.99",
quantity: 1,
},
{
id: "9-8",
productName: "Knock Box",
category: "Kitchen",
price: "$34.99",
quantity: 1,
},
{
id: "9-9",
productName: "Thermometer",
category: "Tools",
price: "$24.99",
quantity: 1,
},
{
id: "9-10",
productName: "Scale",
category: "Tools",
price: "$49.99",
quantity: 1,
},
],
},
{
id: "10",
orderNumber: "ORD-2410",
customer: "Isabella Hayes",
customerEmail: "isabella.hayes@email.com",
customerAvatar: "10.png",
total: "$312.45",
status: {
label: "Shipped",
variant: "primary",
},
items: [
{
id: "10-1",
productName: "Plant Pots Set",
category: "Garden",
price: "$49.99",
quantity: 1,
},
{
id: "10-2",
productName: "Potting Soil",
category: "Garden",
price: "$24.99",
quantity: 2,
},
{
id: "10-3",
productName: "Garden Tools",
category: "Garden",
price: "$39.99",
quantity: 1,
},
{
id: "10-4",
productName: "Watering Can",
category: "Garden",
price: "$29.99",
quantity: 1,
},
{
id: "10-5",
productName: "Plant Food",
category: "Garden",
price: "$19.99",
quantity: 2,
},
{
id: "10-6",
productName: "Garden Gloves",
category: "Garden",
price: "$14.99",
quantity: 1,
},
{
id: "10-7",
productName: "Plant Labels",
category: "Garden",
price: "$9.99",
quantity: 1,
},
{
id: "10-8",
productName: "Spray Bottle",
category: "Garden",
price: "$12.99",
quantity: 1,
},
{
id: "10-9",
productName: "Pruning Shears",
category: "Garden",
price: "$34.99",
quantity: 1,
},
{
id: "10-10",
productName: "Kneeling Pad",
category: "Garden",
price: "$19.99",
quantity: 1,
},
],
},
];
const parseCurrency = (value: string) =>
Number(value.replace(/[^0-9.-]+/g, "")) || 0;
const customerNameMap: Record<string, string> = {
"1": "مانی چن",
"2": "نینا پاتل",
"3": "اولیور مارتینز",
"4": "سوفیا کیم",
"5": "جکسون",
"6": "اما تامپسون",
"7": "لوقاس گونزالز",
"8": "آوا رابینسون",
"9": "ایتان فاستر",
"10": "ایزابلا هایز",
};
const statusLabelMap: Record<string, string> = {
Processing: "در حال پردازش",
Shipped: "ارسال‌شده",
Delivered: "تحویل‌شده",
Pending: "در انتظار پیگیری",
Cancelled: "لغو‌شده",
};
const categoryMap: Record<string, string> = {
Electronics: "الکترونیک",
Accessories: "لوازم جانبی",
Home: "خانه",
Office: "اداری",
Art: "هنر",
Sports: "ورزشی",
Tools: "ابزار",
Clothing: "پوشاک",
Furniture: "مبلمان",
Lighting: "روشنایی",
Kitchen: "آشپزخانه",
Food: "مواد غذایی",
Garden: "باغبانی",
Parts: "قطعات",
};
const productNameMap: Record<string, string> = {
"1-1": "هاب USB-C",
"1-2": "کیف لپ‌تاپ",
"1-3": "SSD قابل حمل",
"1-4": "روکش کابل",
"1-5": "پوشش وب‌کم",
"1-6": "پایه لپ‌تاپ",
"1-7": "بسته کابل USB",
"2-1": "تصفیه‌کننده هوا",
"2-2": "ست روغن‌های ضروری",
"2-3": "رطوبت‌ساز",
"2-4": "اسپری اتاق",
"2-5": "پخش‌کننده رایحه",
"2-6": "شمع‌های معطر",
"2-7": "اسپری بالش",
"2-8": "دماسنج اتاق",
"3-1": "دوربین دیجیتال",
"3-2": "کیف دوربین",
"3-3": "کارت حافظه ۱۲۸ گیگ",
"3-4": "سه‌پایه",
"3-5": "لنز دوربین",
"3-6": "کیت تمیزکننده لنز",
"3-7": "باتری یدک",
"3-8": "بند دوربین",
"3-9": "فیلتر لنز",
"3-10": "ریموت شاتر",
"4-1": "ست لوازم هنری",
"4-2": "دفتر طراحی",
"4-3": "مداد رنگی",
"4-4": "ست آبرنگ",
"4-5": "قلم‌مو",
"4-6": "بسته بوم",
"5-1": "دوچرخه برقی",
"5-2": "کلاه ایمنی دوچرخه",
"5-3": "قفل دوچرخه",
"5-4": "چراغ دوچرخه",
"5-5": "نگهدارنده بطری",
"5-6": "پایه نگهدارنده گوشی",
"5-7": "کیت تعمیر",
"5-8": "پمپ دوچرخه",
"5-9": "زنگ دوچرخه",
"5-10": "جلیقه بازتابنده",
"5-11": "دستکش دوچرخه‌سواری",
"6-1": "جاروبرقی رباتیک",
"6-2": "فیلتر جاروبرقی",
"6-3": "محلول تمیزکننده",
"6-4": "دستمال میکروفایبر",
"6-5": "ست جارو و خاک‌انداز",
"6-6": "سیستم تی",
"6-7": "جارو",
"6-8": "کیسه زباله",
"6-9": "بسته دستکش",
"6-10": "ست اسفنج",
"6-11": "خوشبوکننده هوا",
"6-12": "گردگیر",
"7-1": "کیبورد مکانیکی",
"7-2": "ست کلید",
"7-3": "پد مچ دست",
"7-4": "کشنده سوئیچ",
"7-5": "ثبات‌دهنده",
"7-6": "کابل کیبورد",
"7-7": "برس تمیزکننده",
"7-8": "روغن روان‌کننده",
"7-9": "سوئیچ یدک",
"8-1": "میز ایستاده",
"8-2": "تبدیل‌کننده میز",
"8-3": "بازوی مانیتور",
"8-4": "سینی کابل",
"8-5": "پد میز",
"8-6": "پاورقی",
"8-7": "چراغ LED میز",
"9-1": "دستگاه اسپرسو",
"9-2": "آسیاب قهوه",
"9-3": "پیچر شیر",
"9-4": "دانه قهوه",
"9-5": "تمپر",
"9-6": "قرص تمیزکننده",
"9-7": "محلول رسوب‌زدا",
"9-8": "جعبه ضربه",
"9-9": "دماسنج",
"9-10": "ترازو",
"10-1": "ست گلدان",
"10-2": "خاک گلدان",
"10-3": "ابزار باغبانی",
"10-4": "آبپاش",
"10-5": "کود گیاه",
"10-6": "دستکش باغبانی",
"10-7": "برچسب گیاه",
"10-8": "اسپری آب",
"10-9": "قیچی هرس",
"10-10": "زانوبند",
};
export const validatedData = CustomerRecordSchema.array().parse(
rawOrders.map((order) => {
const parsedItems = order.items.map((item) => ({
...item,
productName: productNameMap[item.id] ?? item.productName,
category: categoryMap[item.category] ?? item.category,
price: parseCurrency(item.price),
}));
const totalItems = parsedItems.reduce(
(acc, item) => acc + item.quantity,
0,
);
return {
id: order.id,
latestOrder: order.orderNumber,
customer: {
name: customerNameMap[order.id] ?? order.customer,
email: order.customerEmail,
avatar: order.customerAvatar,
},
total: parseCurrency(order.total),
totalItems,
status: {
...order.status,
label: statusLabelMap[order.status.label] ?? order.status.label,
},
items: parsedItems,
};
}),
);
const statusBadgeClassMap: Record<StatusVariant, string> = {
info: "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/40 dark:bg-sky-500/10 dark:text-sky-400",
primary:
"border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-500/40 dark:bg-violet-500/10 dark:text-violet-400",
success:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-400",
warning:
"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-400",
destructive:
"border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-500/40 dark:bg-rose-500/10 dark:text-rose-400",
};
const getStatusBadgeClasses = (variant: StatusVariant) =>
statusBadgeClassMap[variant] ?? statusBadgeClassMap.info;
const avatarColorClasses = [
"bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
"bg-emerald-100 text-emerald-700 dark:bg-emerald-800 dark:text-emerald-300",
"bg-indigo-100 text-indigo-700 dark:bg-indigo-800 dark:text-indigo-300",
"bg-amber-100 text-amber-700 dark:bg-amber-800 dark:text-amber-300",
"bg-rose-100 text-rose-700 dark:bg-rose-800 dark:text-rose-300",
"bg-sky-100 text-sky-700 dark:bg-sky-800 dark:text-sky-300",
];
const getAvatarColor = (value: string) => {
const hash = value
.split("")
.reduce((acc, char) => acc + char.charCodeAt(0), 0);
return avatarColorClasses[hash % avatarColorClasses.length];
};
const getInitials = (name: string) => {
const parts = name.trim().split(" ");
const first = parts[0]?.charAt(0) ?? "";
const second = parts[1]?.charAt(0) ?? parts[0]?.charAt(1) ?? "";
return `${first}${second}`;
};
export type StatusOption = {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
};
export const orderStatuses: StatusOption[] = [
{ label: "در حال پردازش", value: "در حال پردازش", icon: LoaderIcon },
{ label: "ارسال‌شده", value: "ارسال‌شده", icon: Truck },
{ label: "تحویل‌شده", value: "تحویل‌شده", icon: CheckCircle },
{ label: "در انتظار پیگیری", value: "در انتظار پیگیری", icon: Clock },
{ label: "لغو‌شده", value: "لغو‌شده", icon: XCircle },
];
const createStatusLookup = (options: StatusOption[]) =>
options.reduce<Record<string, StatusOption>>((acc, option) => {
acc[option.value] = option;
return acc;
}, {});
const orderStatusLookup = createStatusLookup(orderStatuses);
export const getOrderStatusMeta = (status: string) => orderStatusLookup[status];
// Sub-table component for order items
function OrderItemsSubTable({ items }: { items: OrderItem[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
});
const columns = React.useMemo<ColumnDef<OrderItem>[]>(
() => [
{
accessorKey: "productName",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="محصول" />
),
cell: (info) => (
<span className="text-sm font-medium">
{info.getValue() as string}
</span>
),
enableSorting: true,
},
{
accessorKey: "category",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="دسته‌بندی" />
),
cell: (info) => (
<span className="text-sm text-muted-foreground">
{info.getValue() as string}
</span>
),
enableSorting: true,
},
{
accessorKey: "quantity",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="تعداد" />
),
cell: (info) => (
<span className="text-sm font-medium tabular-nums">
{formatNumber(info.getValue() as number)}
</span>
),
meta: { align: "right", className: "tabular-nums" },
enableSorting: true,
},
{
accessorKey: "price",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="قیمت" />
),
cell: (info) => (
<span className="text-sm font-semibold tabular-nums">
{formatPrice(info.getValue() as number)}
</span>
),
meta: { align: "right", className: "tabular-nums" },
enableSorting: true,
},
],
[],
);
const table = useReactTable({
data: items,
columns,
pageCount: Math.ceil(items.length / pagination.pageSize),
state: {
sorting,
pagination,
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getRowId: (row: OrderItem) => row.id,
});
return (
<div className="rounded-lg bg-muted/30 p-4">
<div className="mb-3 flex items-center justify-between">
<h4 className="text-sm font-semibold">اقلام سفارش</h4>
<Badge variant="outline" className="text-xs">
{formatNumber(items.length)} مورد
</Badge>
</div>
<div className="rounded-md border bg-background">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-muted/50">
{headerGroup.headers.map((header) => (
<TableHead key={header.id} className="h-9">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="transition-colors hover:bg-muted/50"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-2">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-16 text-center text-sm text-muted-foreground"
>
اقلامی یافت نشد
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{items.length > pagination.pageSize && (
<div className="mt-3 flex items-center justify-between px-2">
<div className="text-xs text-muted-foreground">
نمایش {formatNumber(table.getRowModel().rows.length)} از{" "}
{formatNumber(items.length)} مورد
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="قبلیی"
>
قبلی
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="بعدیی"
>
بعدی
</Button>
</div>
</div>
)}
</div>
);
}
export const columns: ColumnDef<CustomerRecord>[] = [
{
id: "expand",
cell: ({ row }) => {
const toggleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
row.toggleExpanded();
};
return (
<div className="flex justify-center">
<Button
variant="ghost"
size="icon"
data-state={row.getIsExpanded() ? "open" : "closed"}
onClick={toggleExpand}
aria-label={row.getIsExpanded() ? "بستن جزئیات" : "نمایش جزئیات"}
>
{row.getIsExpanded() ? <SquareMinusIcon /> : <SquarePlusIcon />}
</Button>
</div>
);
},
},
{
accessorKey: "customer",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="مشتری" />
),
cell: ({ row }) => {
const { name, email, avatar } = row.original.customer;
return (
<div className="flex items-center gap-3">
<Avatar className="size-10 border border-border/60">
<AvatarFallback
className={cn(
"text-xs font-semibold uppercase",
getAvatarColor(avatar || name),
)}
>
{getInitials(name)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start gap-0.5">
<span className="text-sm font-medium">{name}</span>
</div>
<span className="text-xs text-muted-foreground">{email}</span>
</div>
);
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "total",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="مجموع" />
),
cell: ({ row }) => (
<span className="text-sm font-semibold tabular-nums">
{formatPrice(row.original.total)}
</span>
),
meta: { align: "right", className: "tabular-nums" },
enableSorting: true,
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="وضعیت معامله" />
),
cell: ({ row }) => {
const status = row.original.status;
const statusMeta = getOrderStatusMeta(status.label);
const StatusIcon = statusMeta?.icon;
return (
<Badge
variant="outline"
className={cn("gap-1", getStatusBadgeClasses(status.variant))}
>
{StatusIcon ? <StatusIcon className="size-3" /> : null}
<span className="text-xs font-medium">{status.label}</span>
</Badge>
);
},
enableSorting: false,
},
{
accessorKey: "totalItems",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="تعداد" />
),
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{formatNumber(row.original.totalItems)} مورد
</span>
),
meta: { align: "right" },
enableSorting: true,
},
];
export function DataTable22({ className }: { className?: string }) {
const { table } = useDataTable({
data: validatedData,
columns,
getRowId: (row) => row.id,
});
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-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
{row.getIsExpanded() && (
<TableRow className="bg-muted/30 transition-colors">
<TableCell className="p-0" colSpan={columns.length}>
<OrderItemsSubTable items={row.original.items} />
</TableCell>
</TableRow>
)}
</React.Fragment>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
نتیجه‌ای یافت نشد
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
</section>
);
}