555 lines
20 KiB
TypeScript
555 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Check,
|
|
ChevronDown,
|
|
Clock,
|
|
MapPin,
|
|
Package,
|
|
RotateCcw,
|
|
Search,
|
|
Truck,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type OrderStatus = "pending" | "shipped" | "delivered" | "cancelled";
|
|
|
|
interface OrderItem {
|
|
name: string;
|
|
variant: string;
|
|
image: string;
|
|
price: number;
|
|
quantity: number;
|
|
}
|
|
|
|
interface ShippingAddress {
|
|
name: string;
|
|
street: string;
|
|
city: string;
|
|
state: string;
|
|
zipCode: string;
|
|
}
|
|
|
|
interface OrderHistoryItem {
|
|
id: string;
|
|
orderNumber: string;
|
|
/** ISO 8601 date (YYYY-MM-DD) */
|
|
orderDate: string;
|
|
status: OrderStatus;
|
|
total: number;
|
|
items: OrderItem[];
|
|
shippingAddress: ShippingAddress;
|
|
trackingNumber?: string;
|
|
/** ISO 8601 date (YYYY-MM-DD) */
|
|
estimatedDelivery?: string;
|
|
}
|
|
|
|
const DEFAULT_ORDERS: OrderHistoryItem[] = [
|
|
{
|
|
id: "1",
|
|
orderNumber: "72831",
|
|
orderDate: "2025-02-15",
|
|
status: "pending",
|
|
total: 109080.0,
|
|
items: [
|
|
{
|
|
name: "چراغ آویز برنجی مدرن",
|
|
variant: "پوشش مات طلایی",
|
|
image:
|
|
"/block-media/shadcnblocks/block/ecommerce/furniture/Modern-Artistic-Lamp-1.png",
|
|
price: 109080.0,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
shippingAddress: {
|
|
name: "علی محمدی",
|
|
street: "خیابان ولیعصر، پلاک ۱۲۳۴، واحد ۵",
|
|
city: "تهران",
|
|
state: "تهران",
|
|
zipCode: "1415783114",
|
|
},
|
|
},
|
|
{
|
|
id: "2",
|
|
orderNumber: "71293",
|
|
orderDate: "2025-02-08",
|
|
status: "shipped",
|
|
total: 159080.0,
|
|
items: [
|
|
{
|
|
name: "صندلی ناهارخوری اسکاندیناوی",
|
|
variant: "بلوط طبیعی",
|
|
image:
|
|
"/block-media/shadcnblocks/block/ecommerce/furniture/Modern-Wooden-Cabinet-1.png",
|
|
price: 159080.0,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
shippingAddress: {
|
|
name: "علی محمدی",
|
|
street: "خیابان ولیعصر، پلاک ۱۲۳۴، واحد ۵",
|
|
city: "تهران",
|
|
state: "تهران",
|
|
zipCode: "1415783114",
|
|
},
|
|
trackingNumber: "1Z999AA10123456784",
|
|
estimatedDelivery: "2025-02-12",
|
|
},
|
|
{
|
|
id: "3",
|
|
orderNumber: "69847",
|
|
orderDate: "2025-01-25",
|
|
status: "delivered",
|
|
total: 449000.0,
|
|
items: [
|
|
{
|
|
name: "کابینت ذخیرهسازی مینسنتری",
|
|
variant: "چوب گردو",
|
|
image:
|
|
"/block-media/shadcnblocks/block/ecommerce/furniture/Minimalist-Wooden-Sideboard-1.png",
|
|
price: 449000.0,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
shippingAddress: {
|
|
name: "علی محمدی",
|
|
street: "خیابان ولیعصر، پلاک ۱۲۳۴، واحد ۵",
|
|
city: "تهران",
|
|
state: "تهران",
|
|
zipCode: "1415783114",
|
|
},
|
|
trackingNumber: "1Z999AA10123456799",
|
|
},
|
|
{
|
|
id: "4",
|
|
orderNumber: "68234",
|
|
orderDate: "2025-01-10",
|
|
status: "delivered",
|
|
total: 289000.0,
|
|
items: [
|
|
{
|
|
name: "چراغ ایستاده مینیمال",
|
|
variant: "نیکل برسخورده",
|
|
image:
|
|
"/block-media/shadcnblocks/block/ecommerce/furniture/Modern-Artistic-Lamp-2.png",
|
|
price: 189000.0,
|
|
quantity: 1,
|
|
},
|
|
{
|
|
name: "گلدان سرامیکی",
|
|
variant: "سفالی",
|
|
image:
|
|
"/block-media/shadcnblocks/block/ecommerce/furniture/Minimalist-Loveseat-1.png",
|
|
price: 100000.0,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
shippingAddress: {
|
|
name: "علی محمدی",
|
|
street: "خیابان ولیعصر، پلاک ۱۲۳۴، واحد ۵",
|
|
city: "تهران",
|
|
state: "تهران",
|
|
zipCode: "1415783114",
|
|
},
|
|
trackingNumber: "1Z999AA10123456800",
|
|
},
|
|
];
|
|
|
|
const formatCurrency = (value: number) =>
|
|
`${value.toLocaleString("fa-IR")} ریال`;
|
|
|
|
const dateFormatter = new Intl.DateTimeFormat("fa-IR", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
const formatOrderDate = (isoDate: string) =>
|
|
dateFormatter.format(new Date(isoDate + "T12:00:00"));
|
|
|
|
const statusConfig: Record<
|
|
OrderStatus,
|
|
{ label: string; className: string; icon: typeof Clock }
|
|
> = {
|
|
pending: {
|
|
label: "در انتظار",
|
|
className:
|
|
"bg-slate-100 text-slate-700 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-300",
|
|
icon: Clock,
|
|
},
|
|
shipped: {
|
|
label: "در مسیر تحویل",
|
|
className:
|
|
"bg-orange-50 text-orange-600 hover:bg-orange-50 dark:bg-orange-950 dark:text-orange-400",
|
|
icon: Truck,
|
|
},
|
|
delivered: {
|
|
label: "تحویلشده",
|
|
className:
|
|
"bg-emerald-50 text-emerald-600 hover:bg-emerald-50 dark:bg-emerald-950 dark:text-emerald-400",
|
|
icon: Check,
|
|
},
|
|
cancelled: {
|
|
label: "لغوشده",
|
|
className:
|
|
"bg-red-50 text-red-600 hover:bg-red-50 dark:bg-red-950 dark:text-red-400",
|
|
icon: Package,
|
|
},
|
|
};
|
|
|
|
const statusFilterLabels: Record<string, string> = {
|
|
all: "همه سفارشها",
|
|
pending: "در انتظار",
|
|
shipped: "در مسیر تحویل",
|
|
delivered: "تحویلشده",
|
|
cancelled: "لغوشده",
|
|
};
|
|
|
|
interface OrderHistory2Props {
|
|
orders?: OrderHistoryItem[];
|
|
className?: string;
|
|
}
|
|
|
|
const OrderHistory2 = ({
|
|
orders = DEFAULT_ORDERS,
|
|
className,
|
|
}: OrderHistory2Props) => {
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
|
|
const filteredOrders = orders.filter((order) => {
|
|
const matchesStatus =
|
|
statusFilter === "all" || order.status === statusFilter;
|
|
const matchesSearch =
|
|
searchQuery === "" ||
|
|
order.orderNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
order.items.some((item) =>
|
|
item.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
);
|
|
return matchesStatus && matchesSearch;
|
|
});
|
|
|
|
return (
|
|
<section
|
|
dir="rtl"
|
|
lang="fa"
|
|
className={cn("py-16 md:py-24", className)}
|
|
>
|
|
<div className="container max-w-5xl">
|
|
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-tight md:text-3xl">
|
|
سفارشهای من
|
|
</h1>
|
|
<p className="mt-1 text-muted-foreground">
|
|
مدیریت، پیگیری و مرور خریدهای خود
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<div className="relative flex-1 sm:flex-initial">
|
|
<Search
|
|
className="absolute top-1/2 start-3 size-4 -translate-y-1/2 text-muted-foreground"
|
|
aria-hidden="true"
|
|
/>
|
|
<Input
|
|
placeholder="جستجو…"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
name="order-history-search"
|
|
inputMode="search"
|
|
autoComplete="off"
|
|
aria-label="جستجوی سفارشها و محصولات"
|
|
className="ps-9 sm:w-48"
|
|
/>
|
|
</div>
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-40" aria-label="فیلتر وضعیت سفارش">
|
|
<SelectValue placeholder={statusFilterLabels.all} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">{statusFilterLabels.all}</SelectItem>
|
|
<SelectItem value="pending">
|
|
{statusFilterLabels.pending}
|
|
</SelectItem>
|
|
<SelectItem value="shipped">
|
|
{statusFilterLabels.shipped}
|
|
</SelectItem>
|
|
<SelectItem value="delivered">
|
|
{statusFilterLabels.delivered}
|
|
</SelectItem>
|
|
<SelectItem value="cancelled">
|
|
{statusFilterLabels.cancelled}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{filteredOrders.map((order) => {
|
|
const status = statusConfig[order.status];
|
|
const StatusIcon = status.icon;
|
|
const isExpanded = expandedId === order.id;
|
|
const primaryItem = order.items[0];
|
|
|
|
return (
|
|
<Collapsible
|
|
key={order.id}
|
|
open={isExpanded}
|
|
onOpenChange={(open) => setExpandedId(open ? order.id : null)}
|
|
>
|
|
<Card className="shadow-none">
|
|
<CollapsibleTrigger asChild>
|
|
<CardContent className="cursor-pointer p-4 md:p-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="size-14 shrink-0 overflow-hidden rounded-lg bg-muted md:size-16">
|
|
<AspectRatio ratio={1}>
|
|
<img
|
|
src={primaryItem.image}
|
|
alt={primaryItem.name}
|
|
className="size-full object-cover"
|
|
/>
|
|
</AspectRatio>
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="font-medium">{primaryItem.name}</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
{primaryItem.variant}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="hidden items-center gap-6 md:flex">
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-primary/10 font-mono text-primary hover:bg-primary/10"
|
|
dir="rtl"
|
|
>
|
|
#{order.orderNumber}
|
|
</Badge>
|
|
<span className="w-32 shrink-0 text-sm text-muted-foreground">
|
|
{formatOrderDate(order.orderDate)}
|
|
</span>
|
|
<Badge className={cn("gap-1.5", status.className)}>
|
|
<StatusIcon className="size-3.5" aria-hidden="true" />
|
|
{status.label}
|
|
</Badge>
|
|
<span className="w-24 shrink-0 text-end font-semibold tabular-nums">
|
|
{formatCurrency(order.total)}
|
|
</span>
|
|
<ChevronDown
|
|
className={cn(
|
|
"size-5 text-muted-foreground transition-transform",
|
|
isExpanded && "rotate-180",
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
|
|
<ChevronDown
|
|
className={cn(
|
|
"size-5 text-muted-foreground transition-transform md:hidden",
|
|
isExpanded && "rotate-180",
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-3 flex flex-wrap items-center gap-2 md:hidden">
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-primary/10 font-mono text-primary hover:bg-primary/10"
|
|
dir="rtl"
|
|
>
|
|
#{order.orderNumber}
|
|
</Badge>
|
|
<span className="text-sm text-muted-foreground">
|
|
{formatOrderDate(order.orderDate)}
|
|
</span>
|
|
<Badge className={cn("gap-1.5", status.className)}>
|
|
<StatusIcon className="size-3.5" aria-hidden="true" />
|
|
{status.label}
|
|
</Badge>
|
|
<span className="ms-auto font-semibold tabular-nums">
|
|
{formatCurrency(order.total)}
|
|
</span>
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<Separator />
|
|
<CardContent className="p-4 md:p-6">
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<div>
|
|
<h4 className="mb-3 text-sm font-medium">
|
|
اقلام سفارش
|
|
</h4>
|
|
<div className="space-y-3">
|
|
{order.items.map((item, index) => (
|
|
<div key={index} className="flex gap-3">
|
|
<div className="size-12 shrink-0 overflow-hidden rounded-md bg-muted">
|
|
<AspectRatio ratio={1}>
|
|
<img
|
|
src={item.image}
|
|
alt={item.name}
|
|
className="size-full object-cover"
|
|
/>
|
|
</AspectRatio>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium">
|
|
{item.name}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{item.variant} · تعداد:{" "}
|
|
{item.quantity.toLocaleString("fa-IR")}
|
|
</p>
|
|
</div>
|
|
<p className="text-sm font-medium tabular-nums">
|
|
{formatCurrency(item.price)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="mb-2 flex items-center gap-2 text-sm font-medium">
|
|
<MapPin
|
|
className="size-4 text-muted-foreground"
|
|
aria-hidden="true"
|
|
/>
|
|
آدرس ارسال
|
|
</h4>
|
|
<div className="rounded-lg bg-muted/50 p-3 text-sm">
|
|
<p className="font-medium">
|
|
{order.shippingAddress.name}
|
|
</p>
|
|
<p className="text-muted-foreground">
|
|
{order.shippingAddress.street}
|
|
</p>
|
|
<p className="text-muted-foreground">
|
|
{order.shippingAddress.city}،{" "}
|
|
{order.shippingAddress.state} کد پستی{" "}
|
|
<span dir="rtl" className="tabular-nums">
|
|
{order.shippingAddress.zipCode}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{order.trackingNumber && (
|
|
<div>
|
|
<h4 className="mb-2 flex items-center gap-2 text-sm font-medium">
|
|
<Truck
|
|
className="size-4 text-muted-foreground"
|
|
aria-hidden="true"
|
|
/>
|
|
رهگیری مرسوله
|
|
</h4>
|
|
<div className="rounded-lg bg-muted/50 p-3 text-sm">
|
|
<p
|
|
className="font-mono text-xs tabular-nums"
|
|
dir="rtl"
|
|
>
|
|
{order.trackingNumber}
|
|
</p>
|
|
{order.estimatedDelivery && (
|
|
<p className="mt-1 text-muted-foreground">
|
|
تحویل تقریبی:{" "}
|
|
{formatOrderDate(order.estimatedDelivery)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex flex-wrap gap-2">
|
|
{order.status === "shipped" && (
|
|
<Button size="sm">
|
|
<Truck className="me-1.5 size-4" aria-hidden="true" />
|
|
پیگیری مرسوله
|
|
</Button>
|
|
)}
|
|
{order.status === "delivered" && (
|
|
<>
|
|
<Button size="sm" variant="outline">
|
|
<RotateCcw
|
|
className="me-1.5 size-4"
|
|
aria-hidden="true"
|
|
/>
|
|
خرید مجدد
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
بازگشت کالا
|
|
</Button>
|
|
</>
|
|
)}
|
|
{order.status === "pending" && (
|
|
<Button size="sm" variant="outline">
|
|
لغو سفارش
|
|
</Button>
|
|
)}
|
|
<Button size="sm" variant="ghost">
|
|
مشاهده فاکتور
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Card>
|
|
</Collapsible>
|
|
);
|
|
})}
|
|
|
|
{filteredOrders.length === 0 && (
|
|
<Card className="shadow-none">
|
|
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
|
<Package
|
|
className="size-8 text-muted-foreground"
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
<h2 className="mb-2 text-lg font-semibold">سفارشی یافت نشد</h2>
|
|
<p className="max-w-sm text-sm text-muted-foreground">
|
|
{searchQuery
|
|
? `سفارشی با عبارت «${searchQuery}» یافت نشد.`
|
|
: "هنوز سفارشی ثبت نکردهاید."}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { OrderHistory2 };
|