973 lines
31 KiB
TypeScript
973 lines
31 KiB
TypeScript
"use client";
|
|
|
|
import type {
|
|
Column,
|
|
ColumnDef,
|
|
Row,
|
|
RowSelectionState,
|
|
SortingState,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getSortedRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
ArrowDown,
|
|
ArrowUp,
|
|
Calendar,
|
|
CheckCheckIcon,
|
|
CheckCircle,
|
|
ChevronLeft,
|
|
ChevronsUpDown,
|
|
Clock,
|
|
LoaderIcon,
|
|
MapPin,
|
|
MoreHorizontal,
|
|
Package,
|
|
PieChart,
|
|
RefreshCcw,
|
|
User,
|
|
} 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 { 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 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>
|
|
);
|
|
}
|
|
|
|
// Customer details schema
|
|
const CustomerSchema = z.object({
|
|
name: z.string(),
|
|
email: z.string(),
|
|
segment: z.string(),
|
|
});
|
|
|
|
// Shipping details schema
|
|
const ShippingSchema = z.object({
|
|
service: z.string(),
|
|
fulfillment_channel: z.string(),
|
|
warehouse: z.string(),
|
|
delivery_eta: z.string(),
|
|
tracking_number: z.string(),
|
|
assigned_to: z.string(),
|
|
});
|
|
|
|
// Order details schema
|
|
const OrderDetailsSchema = z.object({
|
|
payment_method: z.string(),
|
|
items: z.number(),
|
|
coupon_code: z.string().nullable(),
|
|
gift: z.boolean(),
|
|
return_status: z.string(),
|
|
notes: z.string(),
|
|
});
|
|
|
|
export const Orderschema = z.object({
|
|
id: z.string(),
|
|
purchased: z.string(),
|
|
order_status: z.string(),
|
|
payment_status: z.string(),
|
|
order_total: z.number(),
|
|
currency: z.string(),
|
|
priority: z.string(),
|
|
order_date: z.string(),
|
|
last_updated: z.string(),
|
|
risk_level: z.string(),
|
|
customer: CustomerSchema,
|
|
shipping: ShippingSchema,
|
|
orderDetails: OrderDetailsSchema,
|
|
});
|
|
|
|
type Order = z.infer<typeof Orderschema>;
|
|
|
|
export const data = [
|
|
{
|
|
id: "#301456",
|
|
purchased: "کاپشن چرمی",
|
|
order_status: "آماده تحویل",
|
|
customer: "سارا احمدی",
|
|
payment_method: "****7321",
|
|
payment_status: "پرداختشده",
|
|
items: 3,
|
|
order_date: "2025-10-24T08:28:33.060Z",
|
|
delivery_eta: "2025-10-31T08:28:33.060Z",
|
|
fulfillment_channel: "FBA",
|
|
shipping_service: "هوایی سریع",
|
|
warehouse: "ORD-4",
|
|
order_total: 243.4,
|
|
currency: "IRR",
|
|
customer_email: "sarah.johnson@example.com",
|
|
customer_segment: "عمدهفروشی",
|
|
priority: "کم",
|
|
coupon_code: "WELCOME10",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "مژگان بیت",
|
|
risk_level: "کم",
|
|
tracking_number: "TRK5817960062",
|
|
last_updated: "2025-10-24T10:28:33.060Z",
|
|
notes: "سارا احمدی ارسال هوایی سریع را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#789123",
|
|
purchased: "کفش اسپرت آبی",
|
|
order_status: "تحویلشده",
|
|
customer: "رضا محمدی",
|
|
payment_method: "****4509",
|
|
payment_status: "پرداختشده",
|
|
items: 2,
|
|
order_date: "2025-10-22T20:28:33.060Z",
|
|
delivery_eta: "2025-10-25T20:28:33.060Z",
|
|
fulfillment_channel: "شخص ثالث",
|
|
shipping_service: "تحویل یکروزه",
|
|
warehouse: "DFW-2",
|
|
order_total: 119.96,
|
|
currency: "IRR",
|
|
customer_email: "michael.brown@example.com",
|
|
customer_segment: "بازارچه",
|
|
priority: "عادی",
|
|
coupon_code: "VIPONLY",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "هستی سینگ",
|
|
risk_level: "کم",
|
|
tracking_number: "TRK3189925655",
|
|
last_updated: "2025-10-22T22:28:33.060Z",
|
|
notes: "رضا محمدی ارسال تحویل یکروزه را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#654321",
|
|
purchased: "هودی قرمز",
|
|
order_status: "تحویلشده",
|
|
customer: "نیلوفر حسینی",
|
|
payment_method: "****9812",
|
|
payment_status: "در انتظار پیگیری",
|
|
items: 4,
|
|
order_date: "2025-10-21T08:28:33.060Z",
|
|
delivery_eta: "2025-10-28T08:28:33.060Z",
|
|
fulfillment_channel: "FBA",
|
|
shipping_service: "هوایی سریع",
|
|
warehouse: "ATL-1",
|
|
order_total: 354.63,
|
|
currency: "IRR",
|
|
customer_email: "emily.davis@example.com",
|
|
customer_segment: "ویژه",
|
|
priority: "فوری",
|
|
coupon_code: "VIPONLY",
|
|
gift: false,
|
|
return_status: "درخواستشده",
|
|
assigned_to: "هستی سینگ",
|
|
risk_level: "متوسط",
|
|
tracking_number: "TRK2861732425",
|
|
last_updated: "2025-10-21T10:28:33.060Z",
|
|
notes: "نیلوفر حسینی ارسال هوایی سریع را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#987654",
|
|
purchased: "عینک آفتابی مشکی",
|
|
order_status: "تحویلنشده",
|
|
customer: "داوود کریمی",
|
|
payment_method: "****6734",
|
|
payment_status: "در انتظار پیگیری",
|
|
items: 1,
|
|
order_date: "2025-10-19T20:28:33.060Z",
|
|
delivery_eta: "2025-10-22T20:28:33.060Z",
|
|
fulfillment_channel: "شخص ثالث",
|
|
shipping_service: "تحویل یکروزه",
|
|
warehouse: "LAX-5",
|
|
order_total: 119.73,
|
|
currency: "IRR",
|
|
customer_email: "david.lee@example.com",
|
|
customer_segment: "بازارچه",
|
|
priority: "کم",
|
|
coupon_code: "WELCOME10",
|
|
gift: false,
|
|
return_status: "دریافتشده",
|
|
assigned_to: "رایلی نگوین",
|
|
risk_level: "کم",
|
|
tracking_number: "TRK5254503688",
|
|
last_updated: "2025-10-19T22:28:33.060Z",
|
|
notes: "داوود کریمی ارسال تحویل یکروزه را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#432187",
|
|
purchased: "کولهپشتی سبز",
|
|
order_status: "تحویلشده",
|
|
customer: "لیلا رحیمی",
|
|
payment_method: "****2901",
|
|
payment_status: "پرداختشده",
|
|
items: 2,
|
|
order_date: "2025-10-18T08:28:33.060Z",
|
|
delivery_eta: "2025-10-23T08:28:33.060Z",
|
|
fulfillment_channel: "دراپشیپ",
|
|
shipping_service: "تحویل دوروزه",
|
|
warehouse: "ATL-1",
|
|
order_total: 359.28,
|
|
currency: "IRR",
|
|
customer_email: "lisa.wang@example.com",
|
|
customer_segment: "سازمانی",
|
|
priority: "بالا",
|
|
coupon_code: "WELCOME10",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "هستی سینگ",
|
|
risk_level: "کم",
|
|
tracking_number: "TRK2835974731",
|
|
last_updated: "2025-10-18T10:28:33.060Z",
|
|
notes: "لیلا رحیمی ارسال تحویل دوروزه را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#567890",
|
|
purchased: "ساعت نقرهای",
|
|
order_status: "آماده تحویل",
|
|
customer: "امیر نوری",
|
|
payment_method: "****8456",
|
|
payment_status: "در انتظار پیگیری",
|
|
items: 1,
|
|
order_date: "2025-10-16T20:28:33.060Z",
|
|
delivery_eta: "2025-10-22T20:28:33.060Z",
|
|
fulfillment_channel: "دراپشیپ",
|
|
shipping_service: "ارسال عادی",
|
|
warehouse: "ATL-1",
|
|
order_total: 381.57,
|
|
currency: "IRR",
|
|
customer_email: "james.carter@example.com",
|
|
customer_segment: "سازمانی",
|
|
priority: "کم",
|
|
coupon_code: "VIPONLY",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "مژگان بیت",
|
|
risk_level: "بالا",
|
|
tracking_number: "TRK1516495925",
|
|
last_updated: "2025-10-16T22:28:33.060Z",
|
|
notes: "امیر نوری ارسال عادی را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#234567",
|
|
purchased: "شال خاکستری",
|
|
order_status: "تحویلشده",
|
|
customer: "راحله موسوی",
|
|
payment_method: "****1234",
|
|
payment_status: "مسترد شده",
|
|
items: 3,
|
|
order_date: "2025-10-15T08:28:33.060Z",
|
|
delivery_eta: "2025-10-18T08:28:33.060Z",
|
|
fulfillment_channel: "شخص ثالث",
|
|
shipping_service: "تحویل دوروزه",
|
|
warehouse: "JFK-3",
|
|
order_total: 398.12,
|
|
currency: "IRR",
|
|
customer_email: "rachel.kim@example.com",
|
|
customer_segment: "سازمانی",
|
|
priority: "فوری",
|
|
coupon_code: "SPRING24",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "رایلی نگوین",
|
|
risk_level: "متوسط",
|
|
tracking_number: "TRK2114884757",
|
|
last_updated: "2025-10-15T10:28:33.060Z",
|
|
notes: "راحله موسوی ارسال تحویل دوروزه را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#876543",
|
|
purchased: "پیراهن بنفش",
|
|
order_status: "تحویلنشده",
|
|
customer: "توماس علوی",
|
|
payment_method: "****5678",
|
|
payment_status: "مسترد جزئی",
|
|
items: 2,
|
|
order_date: "2025-10-13T20:28:33.060Z",
|
|
delivery_eta: "2025-10-19T20:28:33.060Z",
|
|
fulfillment_channel: "دراپشیپ",
|
|
shipping_service: "تحویل یکروزه",
|
|
warehouse: "ORD-4",
|
|
order_total: 337.01,
|
|
currency: "IRR",
|
|
customer_email: "thomas.allen@example.com",
|
|
customer_segment: "عمدهفروشی",
|
|
priority: "بالا",
|
|
coupon_code: null,
|
|
gift: false,
|
|
return_status: "درخواستشده",
|
|
assigned_to: "مژگان بیت",
|
|
risk_level: "متوسط",
|
|
tracking_number: "TRK0992177244",
|
|
last_updated: "2025-10-13T22:28:33.060Z",
|
|
notes: "توماس علوی ارسال تحویل یکروزه را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#345678",
|
|
purchased: "کلاه سفید",
|
|
order_status: "آماده تحویل",
|
|
customer: "آنا رحیمی",
|
|
payment_method: "****9012",
|
|
payment_status: "پرداختشده",
|
|
items: 1,
|
|
order_date: "2025-10-12T08:28:33.060Z",
|
|
delivery_eta: "2025-10-18T08:28:33.060Z",
|
|
fulfillment_channel: "دراپشیپ",
|
|
shipping_service: "ارسال عادی",
|
|
warehouse: "LAX-5",
|
|
order_total: 277.54,
|
|
currency: "IRR",
|
|
customer_email: "anna.patel@example.com",
|
|
customer_segment: "سازمانی",
|
|
priority: "فوری",
|
|
coupon_code: "WELCOME10",
|
|
gift: false,
|
|
return_status: "تأییدشده",
|
|
assigned_to: "هستی سینگ",
|
|
risk_level: "بالا",
|
|
tracking_number: "TRK8458630889",
|
|
last_updated: "2025-10-12T10:28:33.060Z",
|
|
notes: "آنا رحیمی ارسال عادی را ترجیح میدهد.",
|
|
},
|
|
{
|
|
id: "#456789",
|
|
purchased: "بوت قهوهای",
|
|
order_status: "تحویلشده",
|
|
customer: "حامد سنگری",
|
|
payment_method: "****3456",
|
|
payment_status: "پرداختشده",
|
|
items: 4,
|
|
order_date: "2025-10-10T20:28:33.060Z",
|
|
delivery_eta: "2025-10-13T20:28:33.060Z",
|
|
fulfillment_channel: "ارسال فروشنده",
|
|
shipping_service: "ارسال عادی",
|
|
warehouse: "ATL-1",
|
|
order_total: 377.76,
|
|
currency: "IRR",
|
|
customer_email: "robert.singh@example.com",
|
|
customer_segment: "بازارچه",
|
|
priority: "کم",
|
|
coupon_code: "WELCOME10",
|
|
gift: false,
|
|
return_status: "ندارد",
|
|
assigned_to: "رایلی نگوین",
|
|
risk_level: "متوسط",
|
|
tracking_number: "TRK1335228617",
|
|
last_updated: "2025-10-10T22:28:33.060Z",
|
|
notes: "حامد سنگری ارسال عادی را ترجیح میدهد.",
|
|
},
|
|
];
|
|
|
|
// Define the raw data interface
|
|
interface RawOrderData {
|
|
id: string;
|
|
purchased: string;
|
|
order_status: string;
|
|
customer: string;
|
|
payment_method: string;
|
|
payment_status: string;
|
|
items: number;
|
|
order_date: string;
|
|
delivery_eta: string;
|
|
fulfillment_channel: string;
|
|
shipping_service: string;
|
|
warehouse: string;
|
|
order_total: number;
|
|
currency: string;
|
|
customer_email: string;
|
|
customer_segment: string;
|
|
priority: string;
|
|
coupon_code: string | null;
|
|
gift: boolean;
|
|
return_status: string;
|
|
assigned_to: string;
|
|
risk_level: string;
|
|
tracking_number: string;
|
|
last_updated: string;
|
|
notes: string;
|
|
}
|
|
|
|
// Transform flat data into nested structure
|
|
export const transformedData = data.map((item: RawOrderData) => ({
|
|
id: item.id,
|
|
purchased: item.purchased,
|
|
order_status: item.order_status,
|
|
payment_status: item.payment_status,
|
|
order_total: item.order_total,
|
|
currency: item.currency,
|
|
priority: item.priority,
|
|
order_date: item.order_date,
|
|
last_updated: item.last_updated,
|
|
risk_level: item.risk_level,
|
|
customer: {
|
|
name: item.customer,
|
|
email: item.customer_email,
|
|
segment: item.customer_segment,
|
|
},
|
|
shipping: {
|
|
service: item.shipping_service,
|
|
fulfillment_channel: item.fulfillment_channel,
|
|
warehouse: item.warehouse,
|
|
delivery_eta: item.delivery_eta,
|
|
tracking_number: item.tracking_number,
|
|
assigned_to: item.assigned_to,
|
|
},
|
|
orderDetails: {
|
|
payment_method: item.payment_method,
|
|
items: item.items,
|
|
coupon_code: item.coupon_code,
|
|
gift: item.gift,
|
|
return_status: item.return_status,
|
|
notes: item.notes,
|
|
},
|
|
}));
|
|
|
|
export const validatedData = Orderschema.array().parse(transformedData);
|
|
|
|
const dateFormatter = new Intl.DateTimeFormat("fa-IR", {
|
|
dateStyle: "medium",
|
|
});
|
|
|
|
const formatDate = (value: string) => {
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? value : dateFormatter.format(date);
|
|
};
|
|
|
|
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);
|
|
|
|
export const getOrderStatusMeta = (status: string) => orderStatusLookup[status];
|
|
|
|
export const getStatusVariant = (status: string) => {
|
|
switch (status) {
|
|
case "تحویلشده":
|
|
return "default";
|
|
case "آماده تحویل":
|
|
return "secondary";
|
|
case "تحویلنشده":
|
|
return "outline";
|
|
default:
|
|
return "outline";
|
|
}
|
|
};
|
|
|
|
export const columns: ColumnDef<Order>[] = [
|
|
{
|
|
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-[100px] font-mono text-sm">{row.original.id}</div>
|
|
),
|
|
enableSorting: true,
|
|
enableHiding: false,
|
|
},
|
|
{
|
|
accessorKey: "purchased",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="محصول" />
|
|
),
|
|
cell: ({ row }) => (
|
|
<div className="max-w-[200px] truncate text-sm font-medium">
|
|
{row.original.purchased}
|
|
</div>
|
|
),
|
|
enableSorting: false,
|
|
enableHiding: false,
|
|
},
|
|
|
|
{
|
|
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 (
|
|
<Badge variant={getStatusVariant(status)} className="gap-1">
|
|
{StatusIcon ? <StatusIcon className="size-3" /> : null}
|
|
<span className="text-xs">{statusMeta?.label ?? status}</span>
|
|
</Badge>
|
|
);
|
|
},
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "order_total",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="مجموع" />
|
|
),
|
|
cell: ({ row }) => (
|
|
<span className="text-sm font-medium tabular-nums">
|
|
{formatPrice(row.original.order_total)}
|
|
</span>
|
|
),
|
|
meta: { align: "right", className: "tabular-nums" },
|
|
enableSorting: true,
|
|
},
|
|
|
|
{
|
|
accessorKey: "customer",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="جزئیات سفارش" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const expandHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
e.stopPropagation();
|
|
row.toggleExpanded();
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={cn(
|
|
"group w-full justify-start gap-2 transition-colors",
|
|
"cursor-pointer border-primary/20 hover:bg-primary/5",
|
|
)}
|
|
onClick={expandHandler}
|
|
>
|
|
<div className="truncate text-sm font-medium">
|
|
{row.original.customer.name}
|
|
</div>
|
|
<div className="truncate text-xs text-muted-foreground">
|
|
{row.original.customer.segment}
|
|
</div>
|
|
|
|
<ChevronLeft
|
|
className={cn(
|
|
"ms-auto h-4 w-4 text-primary transition-transform duration-200 rtl:rotate-180",
|
|
row.getIsExpanded() && "rotate-90 transform",
|
|
)}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => (
|
|
<div className="flex justify-center">
|
|
<DataTableRowActions
|
|
row={row}
|
|
schema={Orderschema}
|
|
itemName="سفارش"
|
|
idField="id"
|
|
/>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
export function DataTable16({ className }: { className?: string }) {
|
|
const { table } = useDataTable({
|
|
data: validatedData,
|
|
columns,
|
|
getRowId: (row) => row.id.toString(),
|
|
});
|
|
|
|
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/50 transition-colors">
|
|
<TableCell className="p-0" colSpan={5} />
|
|
<TableCell className="py-4" colSpan={1}>
|
|
<div className="rounded-lg border border-border/50 bg-card p-4 shadow-sm">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* Left Column */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-primary/10 p-2">
|
|
<Package className="h-4 w-4 text-primary" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
سرویس حمل
|
|
</p>
|
|
<p className="text-sm font-medium">
|
|
{row.original.shipping.service}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-muted p-2">
|
|
<MapPin className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
رهگیری
|
|
</p>
|
|
<p className="rounded bg-muted px-2 py-1 font-mono text-sm font-medium">
|
|
{row.original.shipping.tracking_number}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-muted p-2">
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
تحویل
|
|
</p>
|
|
<p className="text-sm font-medium">
|
|
{formatDate(
|
|
row.original.shipping.delivery_eta,
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-muted p-2">
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
انبار
|
|
</p>
|
|
<p className="text-sm font-medium">
|
|
{row.original.shipping.warehouse}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-muted p-2">
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
کانال ارسال
|
|
</p>
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs"
|
|
>
|
|
{
|
|
row.original.shipping
|
|
.fulfillment_channel
|
|
}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="rounded-md bg-muted p-2">
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
مسئول
|
|
</p>
|
|
<p className="text-sm font-medium">
|
|
{row.original.shipping.assigned_to}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="p-0" colSpan={1} />
|
|
</TableRow>
|
|
)}
|
|
</React.Fragment>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
نتیجهای یافت نشد
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|