Files
reui-blocks/blocks/dashboard10.tsx
T

2081 lines
63 KiB
TypeScript
Raw Normal View History

"use client";
import {
BarChart3,
Bell,
Box,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsUpDown,
ClipboardList,
FileText,
Globe,
LayoutDashboard,
LineChart,
LogOut,
Menu,
MessageSquare,
Minus,
Package,
RefreshCw,
RotateCcw,
Search,
Settings,
ShoppingCart,
Star,
Tag,
TrendingDown,
TrendingUp,
Truck,
User,
Users,
} from "lucide-react";
import * as React from "react";
import type { TooltipProps } from "recharts";
import {
Bar,
BarChart,
CartesianGrid,
Scatter,
ScatterChart,
Tooltip,
XAxis,
YAxis,
ZAxis,
} from "recharts";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
type NavItem = {
label: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
href: string;
isActive?: boolean;
children?: NavItem[];
};
type NavGroup = {
title: string;
items: NavItem[];
};
type UserData = {
name: string;
email: string;
avatar: string;
};
type ShellData = {
logo: {
src: string;
alt: string;
title: string;
};
navGroups: NavGroup[];
footerGroup: NavGroup;
user: UserData;
};
type RichMetricStatItem = {
title: string;
value: number;
trendValue: number;
footerDelta: number;
footerSubtextCount: number;
};
type DailyMetric = {
day: string;
orderCount: number;
avgOrderValue: number;
totalRevenue: number;
};
type ChannelRevenue = {
month: string;
online: number;
inStore: number;
wholesale: number;
marketplace: number;
};
type ActivityEvent = {
id: string;
type: "order" | "refund" | "stock" | "review" | "shipment";
message: string;
timestamp: Date;
};
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
type Order = {
id: string;
orderNumber: string;
customer: string;
total: number;
status: OrderStatus;
date: string;
};
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
});
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
notation: "compact",
maximumFractionDigits: 0,
});
const numberFormatter = new Intl.NumberFormat("fa-IR");
const oneDecimalFormatter = new Intl.NumberFormat("fa-IR", {
maximumFractionDigits: 1,
});
const trendPercentFormatter = new Intl.NumberFormat("fa-IR", {
style: "percent",
maximumFractionDigits: 1,
signDisplay: "exceptZero",
});
const shortDateFormatter = new Intl.DateTimeFormat("fa-IR", {
month: "short",
day: "numeric",
});
const timeFormatter = new Intl.DateTimeFormat("fa-IR", {
hour: "numeric",
minute: "2-digit",
});
const formatTrendValue = (trendValue: number) => {
if (trendValue === 0) return "۰٪";
return trendPercentFormatter.format(trendValue / 100);
};
const orderStatusLabels: Record<OrderStatus, string> = {
Processing: "در حال پردازش",
Shipped: "ارسال‌شده",
Delivered: "تحویل‌شده",
Cancelled: "لغوشده",
};
const mixBase = "transparent";
const palette = {
primary: "var(--primary)",
secondary: {
light: `color-mix(in oklch, var(--primary) 75%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 85%, ${mixBase})`,
},
tertiary: {
light: `color-mix(in oklch, var(--primary) 55%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 65%, ${mixBase})`,
},
quaternary: {
light: `color-mix(in oklch, var(--primary) 40%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 45%, ${mixBase})`,
},
};
const shellData: ShellData = {
logo: {
src: "/block-media/shadcnblocks/block/logos/NovaSales-logo.svg",
alt: "فروشگاه آکمه",
title: "ری یو آی Store",
},
navGroups: [
{
title: "داشبورد",
items: [
{
label: "نمای کلی",
icon: LayoutDashboard,
href: "#",
isActive: true,
},
{ label: "بلادرنگ", icon: RefreshCw, href: "#" },
{ label: "گزارش‌ها", icon: BarChart3, href: "#" },
],
},
{
title: "سفارش‌ها",
items: [
{ label: "همه سفارش‌ها", icon: ClipboardList, href: "#" },
{ label: "مرجوعی‌ها", icon: RotateCcw, href: "#" },
{ label: "ارسال", icon: Truck, href: "#" },
{ label: "تکمیل", icon: Package, href: "#" },
],
},
{
title: "محصولات",
items: [
{
label: "کاتالوگ",
icon: Box,
href: "#",
children: [
{ label: "همه محصولات", icon: FileText, href: "#" },
{ label: "دسته‌بندی‌ها", icon: FileText, href: "#" },
],
},
{ label: "موجودی", icon: Package, href: "#" },
{ label: "قیمت‌گذاری", icon: Tag, href: "#" },
],
},
{
title: "مشتریان",
items: [
{ label: "همه مشتریان", icon: Users, href: "#" },
{ label: "بخش‌ها", icon: BarChart3, href: "#" },
{ label: "پیام‌ها", icon: MessageSquare, href: "#" },
],
},
{
title: "تحلیل فروش",
items: [
{ label: "تحلیل فروش", icon: LineChart, href: "#" },
{ label: "ترافیک", icon: Globe, href: "#" },
{ label: "تبدیل", icon: TrendingUp, href: "#" },
],
},
],
footerGroup: {
title: "تنظیمات",
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
},
user: {
name: "علی رضایی",
email: "john@acme.store",
avatar:
"/block-media/shadcnblocks/block/avatar-4.webp",
},
};
const richMetricStats: RichMetricStatItem[] = [
{
title: "درآمد کل",
value: 1_284_350,
trendValue: 8.2,
footerDelta: 97_420,
footerSubtextCount: 18_450,
},
{
title: "سفارش‌های امروز",
value: 342,
trendValue: 12.5,
footerDelta: 38,
footerSubtextCount: 342,
},
{
title: "میانگین ارزش سفارش",
value: 127.45,
trendValue: -2.3,
footerDelta: -3.05,
footerSubtextCount: 9_800,
},
{
title: "نرخ تبدیل",
value: 3.42,
trendValue: 0.8,
footerDelta: 0.03,
footerSubtextCount: 145_000,
},
];
const dailyMetrics: DailyMetric[] = [
{ day: "Jan 1", orderCount: 142, avgOrderValue: 89, totalRevenue: 12638 },
{ day: "Jan 2", orderCount: 198, avgOrderValue: 112, totalRevenue: 22176 },
{ day: "Jan 3", orderCount: 167, avgOrderValue: 95, totalRevenue: 15865 },
{ day: "Jan 4", orderCount: 245, avgOrderValue: 78, totalRevenue: 19110 },
{ day: "Jan 5", orderCount: 289, avgOrderValue: 134, totalRevenue: 38726 },
{ day: "Jan 6", orderCount: 112, avgOrderValue: 156, totalRevenue: 17472 },
{ day: "Jan 7", orderCount: 178, avgOrderValue: 102, totalRevenue: 18156 },
{ day: "Jan 8", orderCount: 234, avgOrderValue: 88, totalRevenue: 20592 },
{ day: "Jan 9", orderCount: 156, avgOrderValue: 145, totalRevenue: 22620 },
{ day: "Jan 10", orderCount: 267, avgOrderValue: 110, totalRevenue: 29370 },
{ day: "Jan 11", orderCount: 189, avgOrderValue: 167, totalRevenue: 31563 },
{ day: "Jan 12", orderCount: 145, avgOrderValue: 92, totalRevenue: 13340 },
{ day: "Jan 13", orderCount: 278, avgOrderValue: 125, totalRevenue: 34750 },
{ day: "Jan 14", orderCount: 201, avgOrderValue: 138, totalRevenue: 27738 },
{ day: "Jan 15", orderCount: 134, avgOrderValue: 176, totalRevenue: 23584 },
{ day: "Jan 16", orderCount: 256, avgOrderValue: 99, totalRevenue: 25344 },
{ day: "Jan 17", orderCount: 187, avgOrderValue: 142, totalRevenue: 26554 },
{ day: "Jan 18", orderCount: 223, avgOrderValue: 85, totalRevenue: 18955 },
{ day: "Jan 19", orderCount: 165, avgOrderValue: 118, totalRevenue: 19470 },
{ day: "Jan 20", orderCount: 298, avgOrderValue: 105, totalRevenue: 31290 },
{ day: "Jan 21", orderCount: 176, avgOrderValue: 162, totalRevenue: 28512 },
{ day: "Jan 22", orderCount: 210, avgOrderValue: 94, totalRevenue: 19740 },
{ day: "Jan 23", orderCount: 243, avgOrderValue: 128, totalRevenue: 31104 },
{ day: "Jan 24", orderCount: 159, avgOrderValue: 151, totalRevenue: 24009 },
{ day: "Jan 25", orderCount: 281, avgOrderValue: 87, totalRevenue: 24447 },
{ day: "Jan 26", orderCount: 195, avgOrderValue: 143, totalRevenue: 27885 },
{ day: "Jan 27", orderCount: 128, avgOrderValue: 189, totalRevenue: 24192 },
{ day: "Jan 28", orderCount: 267, avgOrderValue: 96, totalRevenue: 25632 },
{ day: "Jan 29", orderCount: 214, avgOrderValue: 131, totalRevenue: 28034 },
{ day: "Jan 30", orderCount: 183, avgOrderValue: 117, totalRevenue: 21411 },
];
const dayLabelFormatter = new Intl.DateTimeFormat("fa-IR", {
month: "short",
day: "numeric",
});
dailyMetrics.forEach((entry, index) => {
entry.day = dayLabelFormatter.format(new Date(2025, 0, index + 1));
});
const monthFormatter = new Intl.DateTimeFormat("fa-IR", { month: "short" });
const monthLabel = (monthIndex: number) =>
monthFormatter.format(new Date(2025, monthIndex, 1));
const channelRevenueData: ChannelRevenue[] = [
{
month: monthLabel(0),
online: 42000,
inStore: 18000,
wholesale: 12000,
marketplace: 8000,
},
{
month: monthLabel(1),
online: 38000,
inStore: 21000,
wholesale: 14000,
marketplace: 9500,
},
{
month: monthLabel(2),
online: 51000,
inStore: 19000,
wholesale: 11000,
marketplace: 7200,
},
{
month: monthLabel(3),
online: 45000,
inStore: 22000,
wholesale: 15000,
marketplace: 10800,
},
{
month: monthLabel(4),
online: 58000,
inStore: 24000,
wholesale: 13000,
marketplace: 11500,
},
{
month: monthLabel(5),
online: 62000,
inStore: 20000,
wholesale: 16000,
marketplace: 12300,
},
];
const GAP_OFFSET = 2500;
const fullYearData = [
{
quarter: "س۱ '۲۱",
thisYear: 28000,
lastYear: -18000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۲ '۲۱",
thisYear: 35000,
lastYear: -22000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۳ '۲۱",
thisYear: 32000,
lastYear: -20000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۱ '۲۲",
thisYear: 42000,
lastYear: -25000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۲ '۲۲",
thisYear: 48000,
lastYear: -28000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۳ '۲۲",
thisYear: 45000,
lastYear: -24000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۱ '۲۳",
thisYear: 55000,
lastYear: -30000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۲ '۲۳",
thisYear: 62000,
lastYear: -32000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۳ '۲۳",
thisYear: 58000,
lastYear: -27000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۱ '۲۴",
thisYear: 65000,
lastYear: -35000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۲ '۲۴",
thisYear: 70000,
lastYear: -33000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
{
quarter: "س۳ '۲۴",
thisYear: 68000,
lastYear: -30000,
gapUp: GAP_OFFSET,
gapDown: -GAP_OFFSET,
},
];
const revenueDomainMax =
Math.ceil(
Math.max(
...fullYearData.map((entry) =>
Math.max(
Math.abs(entry.thisYear) + GAP_OFFSET,
Math.abs(entry.lastYear) + GAP_OFFSET,
),
),
) / 5000,
) * 5000;
const activityEvents: ActivityEvent[] = [
{
id: "1",
type: "order",
message: "سفارش جدید #ORD-4892 توسط مریم J. ثبت شد",
timestamp: new Date(2025, 0, 31, 14, 32),
},
{
id: "2",
type: "shipment",
message: "سفارش #ORD-4888 با FedEx ارسال شد",
timestamp: new Date(2025, 0, 31, 13, 15),
},
{
id: "3",
type: "review",
message: "امتیاز ۵ ستاره برای Wireless Headphones حرفه‌ای",
timestamp: new Date(2025, 0, 31, 12, 48),
},
{
id: "4",
type: "refund",
message: "بازپرداخت سفارش #ORD-4870 انجام شد",
timestamp: new Date(2025, 0, 31, 11, 22),
},
{
id: "5",
type: "stock",
message: "هشدار موجودی کم: USB-C Charger (۱۲ عدد)",
timestamp: new Date(2025, 0, 31, 10, 55),
},
{
id: "6",
type: "order",
message: "سفارش جدید #ORD-4891 توسط Michael C. ثبت شد",
timestamp: new Date(2025, 0, 31, 10, 10),
},
{
id: "7",
type: "shipment",
message: "سفارش #ORD-4885 با موفقیت تحویل شد",
timestamp: new Date(2025, 0, 31, 9, 45),
},
{
id: "8",
type: "review",
message: "امتیاز ۴ ستاره برای Organic Cotton Tee",
timestamp: new Date(2025, 0, 31, 9, 20),
},
{
id: "9",
type: "stock",
message: "هشدار موجودی کم: Yoga Mat Premium (۵ عدد)",
timestamp: new Date(2025, 0, 31, 8, 30),
},
{
id: "10",
type: "order",
message: "سفارش جدید #ORD-4890 توسط Emma W. ثبت شد",
timestamp: new Date(2025, 0, 31, 8, 5),
},
{
id: "11",
type: "refund",
message: "درخواست بازپرداخت برای سفارش #ORD-4862",
timestamp: new Date(2025, 0, 31, 7, 40),
},
{
id: "12",
type: "shipment",
message: "سفارش #ORD-4882 در مسیر تحویل است",
timestamp: new Date(2025, 0, 31, 7, 15),
},
];
const activityIcons: Record<
ActivityEvent["type"],
React.ComponentType<React.SVGProps<SVGSVGElement>>
> = {
order: ShoppingCart,
refund: RotateCcw,
stock: Package,
review: Star,
shipment: Truck,
};
const activityColors: Record<ActivityEvent["type"], string> = {
order:
"text-emerald-600 bg-emerald-50 dark:text-emerald-400 dark:bg-emerald-900/30",
refund: "text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-900/30",
stock: "text-amber-600 bg-amber-50 dark:text-amber-400 dark:bg-amber-900/30",
review: "text-blue-600 bg-blue-50 dark:text-blue-400 dark:bg-blue-900/30",
shipment:
"text-violet-600 bg-violet-50 dark:text-violet-400 dark:bg-violet-900/30",
};
const orderStatuses: OrderStatus[] = [
"Processing",
"Shipped",
"Delivered",
"Cancelled",
];
const statusStyles: Record<OrderStatus, string> = {
Processing:
"bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-400/20",
Shipped:
"bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20",
Delivered:
"bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-900/30 dark:text-emerald-400 dark:ring-emerald-400/20",
Cancelled:
"bg-red-50 text-red-700 ring-1 ring-inset ring-red-600/10 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-400/20",
};
const orders: Order[] = [
{
id: "1",
orderNumber: "ORD-2025-001",
customer: "Sarah Johnson",
status: "Delivered",
total: 2499.0,
date: "2025-01-31",
},
{
id: "2",
orderNumber: "ORD-2025-002",
customer: "Michael Chen",
status: "Shipped",
total: 1348.0,
date: "2025-01-30",
},
{
id: "3",
orderNumber: "ORD-2025-003",
customer: "Emma Wilson",
status: "Processing",
total: 1198.0,
date: "2025-01-30",
},
{
id: "4",
orderNumber: "ORD-2025-004",
customer: "James Rodriguez",
status: "Delivered",
total: 799.0,
date: "2025-01-29",
},
{
id: "5",
orderNumber: "ORD-2025-005",
customer: "Lisa Park",
status: "Cancelled",
total: 599.0,
date: "2025-01-29",
},
{
id: "6",
orderNumber: "ORD-2025-006",
customer: "David Kim",
status: "Shipped",
total: 5498.0,
date: "2025-01-28",
},
{
id: "7",
orderNumber: "ORD-2025-007",
customer: "Anna Martinez",
status: "Delivered",
total: 1199.0,
date: "2025-01-28",
},
{
id: "8",
orderNumber: "ORD-2025-008",
customer: "Robert Taylor",
status: "Processing",
total: 1128.0,
date: "2025-01-27",
},
{
id: "9",
orderNumber: "ORD-2025-009",
customer: "Jennifer Lee",
status: "Shipped",
total: 449.0,
date: "2025-01-27",
},
{
id: "10",
orderNumber: "ORD-2025-010",
customer: "William Brown",
status: "Delivered",
total: 2199.0,
date: "2025-01-26",
},
{
id: "11",
orderNumber: "ORD-2025-011",
customer: "Sophia Davis",
status: "Cancelled",
total: 349.0,
date: "2025-01-26",
},
{
id: "12",
orderNumber: "ORD-2025-012",
customer: "Daniel Garcia",
status: "Processing",
total: 899.0,
date: "2025-01-25",
},
{
id: "13",
orderNumber: "ORD-2025-013",
customer: "Olivia White",
status: "Delivered",
total: 3250.0,
date: "2025-01-25",
},
{
id: "14",
orderNumber: "ORD-2025-014",
customer: "Thomas Harris",
status: "Shipped",
total: 742.0,
date: "2025-01-24",
},
{
id: "15",
orderNumber: "ORD-2025-015",
customer: "Rachel Clark",
status: "Processing",
total: 1875.0,
date: "2025-01-24",
},
{
id: "16",
orderNumber: "ORD-2025-016",
customer: "Kevin Lewis",
status: "Delivered",
total: 564.0,
date: "2025-01-23",
},
{
id: "17",
orderNumber: "ORD-2025-017",
customer: "Amanda Robinson",
status: "Cancelled",
total: 2100.0,
date: "2025-01-23",
},
{
id: "18",
orderNumber: "ORD-2025-018",
customer: "Chris Walker",
status: "Shipped",
total: 1450.0,
date: "2025-01-22",
},
{
id: "19",
orderNumber: "ORD-2025-019",
customer: "Natalie Young",
status: "Delivered",
total: 890.0,
date: "2025-01-22",
},
{
id: "20",
orderNumber: "ORD-2025-020",
customer: "Brian Allen",
status: "Processing",
total: 3675.0,
date: "2025-01-21",
},
];
const PAGE_SIZE_OPTIONS = [10, 25, 50];
const scatterChartConfig = {
scatter: { label: "شاخص‌های روزانه", color: palette.primary },
} satisfies ChartConfig;
const channelChartConfig = {
online: { label: "آنلاین", color: palette.primary },
inStore: { label: "حضوری", theme: palette.secondary },
wholesale: { label: "عمده‌فروشی", theme: palette.tertiary },
marketplace: { label: "مارکت‌پلیس", theme: palette.quaternary },
} satisfies ChartConfig;
const revenueChartConfig = {
thisYear: { label: "امسال", color: palette.primary },
lastYear: { label: "سال قبل", theme: palette.secondary },
} satisfies ChartConfig;
const chartHeaderClass =
"flex h-14 items-center justify-between border-b px-4 sm:px-5";
const chartIconBtnClass = "size-7 sm:size-8";
const chartIconClass = "size-4 text-muted-foreground sm:size-[18px]";
const chartTitleClass = "text-sm font-medium text-pretty sm:text-base";
const tooltipLabelClass = "text-[10px] text-muted-foreground sm:text-xs";
const tooltipValueClass = "text-[10px] font-medium text-foreground sm:text-xs";
const NavDropdown = ({
group,
isActive,
onSelect,
}: {
group: NavGroup;
isActive: boolean;
onSelect: () => void;
}) => {
return (
<Button
variant="ghost"
className={cn("gap-1", isActive && "bg-muted")}
onClick={onSelect}
>
{group.title}
</Button>
);
};
const SubNav = ({
group,
activeItem,
onSelectItem,
}: {
group: NavGroup;
activeItem: NavItem | null;
onSelectItem: (item: NavItem) => void;
}) => {
return (
<div className="border-b bg-muted/30">
<div className="mx-auto flex h-10 w-full max-w-7xl items-center gap-1 px-4 lg:px-6">
{group.items.map((item) => {
const Icon = item.icon;
const isActive = activeItem?.label === item.label;
const hasChildren = item.children && item.children.length > 0;
if (hasChildren) {
return (
<DropdownMenu key={item.label}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className={cn(
"h-7 gap-1.5 text-sm",
isActive && "bg-accent text-accent-foreground",
)}
>
<Icon className="size-4" aria-hidden="true" />
{item.label}
<ChevronDown className="size-3" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{item.children!.map((child) => {
const ChildIcon = child.icon;
return (
<DropdownMenuItem key={child.label} asChild>
<a
href={child.href}
className="flex items-center gap-2"
>
<ChildIcon className="size-4" aria-hidden="true" />
{child.label}
</a>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
return (
<Button
key={item.label}
variant="ghost"
size="sm"
className={cn(
"h-7 gap-1.5 text-sm",
isActive && "bg-accent text-accent-foreground",
)}
onClick={() => onSelectItem(item)}
>
<Icon className="size-4" aria-hidden="true" />
{item.label}
</Button>
);
})}
</div>
</div>
);
};
const MobileNav = ({
activeGroupIndex,
onSelectGroup,
}: {
activeGroupIndex: number;
onSelectGroup: (index: number) => void;
}) => {
return (
<Sheet>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className="md:hidden"
aria-label="باز کردن منوی ناوبری"
>
<Menu className="size-5" aria-hidden="true" />
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-72">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<img
src={shellData.logo.src}
alt={shellData.logo.alt}
className="size-6"
/>
{shellData.logo.title}
</SheetTitle>
</SheetHeader>
<nav className="mt-6 flex flex-col gap-4">
{shellData.navGroups.map((group, index) => (
<div key={group.title}>
<button
onClick={() => onSelectGroup(index)}
className={cn(
"mb-2 text-xs font-medium tracking-wider uppercase",
index === activeGroupIndex
? "text-foreground"
: "text-muted-foreground",
)}
>
{group.title}
</button>
{index === activeGroupIndex && (
<div className="flex flex-col gap-1">
{group.items.map((item) => {
const Icon = item.icon;
return (
<React.Fragment key={item.label}>
<a
href={item.href}
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted",
item.isActive && "bg-muted font-medium",
)}
>
<Icon className="size-4" aria-hidden="true" />
{item.label}
</a>
{item.children?.map((child) => {
const ChildIcon = child.icon;
return (
<a
key={child.label}
href={child.href}
className="flex items-center gap-2 rounded-md py-1.5 pe-2 ps-8 text-sm hover:bg-muted"
>
<ChildIcon
className="size-4"
aria-hidden="true"
/>
{child.label}
</a>
);
})}
</React.Fragment>
);
})}
</div>
)}
</div>
))}
<div className="mt-auto border-t pt-4">
{shellData.footerGroup.items.map((item) => {
const Icon = item.icon;
return (
<a
key={item.label}
href={item.href}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-muted"
>
<Icon className="size-4" aria-hidden="true" />
{item.label}
</a>
);
})}
</div>
</nav>
</SheetContent>
</Sheet>
);
};
const NavUser = ({ user }: { user: UserData }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="gap-2 px-2">
<Avatar className="size-8">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>
{user.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<span className="hidden text-sm font-medium md:inline">
{user.name}
</span>
<ChevronsUpDown
className="hidden size-4 md:block"
aria-hidden="true"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium">{user.name}</p>
<p className="text-xs text-muted-foreground">{user.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="me-2 size-4" aria-hidden="true" />
حساب کاربری
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut className="me-2 size-4" aria-hidden="true" />
خروج
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
const DashboardHeader = () => {
return (
<div className="flex flex-col gap-0.5">
<h1 className="text-lg font-semibold sm:text-xl">صبح بخیر، جیمز.</h1>
<p className="text-sm text-muted-foreground">
وضعیت فروشگاه شما در امروز:
</p>
</div>
);
};
const StatCards = () => {
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{richMetricStats.map((stat) => {
const isPositive = stat.trendValue > 0;
const isNeutral = stat.trendValue === 0;
const Icon = isNeutral ? Minus : isPositive ? TrendingUp : TrendingDown;
const trendClass = isNeutral
? "text-foreground bg-muted"
: isPositive
? "text-success border-success/20 bg-success/10"
: "text-destructive border-destructive/20 bg-destructive/10";
const formattedTrend = formatTrendValue(stat.trendValue);
let formattedValue: string;
if (stat.title === "نرخ تبدیل") {
formattedValue = `${oneDecimalFormatter.format(stat.value)}٪`;
} else if (stat.title === "سفارش‌های امروز") {
formattedValue = numberFormatter.format(stat.value);
} else {
formattedValue = currencyFormatter.format(stat.value);
}
let footerLabel: string;
if (stat.title === "نرخ تبدیل") {
const deltaPrefix = stat.footerDelta > 0 ? "+" : "";
footerLabel = `${deltaPrefix}${oneDecimalFormatter.format(stat.footerDelta)}٪ نسبت به ماه قبل`;
} else if (stat.title === "سفارش‌های امروز") {
const deltaPrefix = stat.footerDelta > 0 ? "+" : "";
footerLabel = `${deltaPrefix}${numberFormatter.format(stat.footerDelta)} نسبت به دیروز`;
} else {
const deltaPrefix =
stat.footerDelta > 0 ? "+" : stat.footerDelta < 0 ? "-" : "";
footerLabel = `${deltaPrefix}${currencyFormatter.format(Math.abs(stat.footerDelta))} نسبت به ماه قبل`;
}
const footerSubtext = `بر اساس ${numberFormatter.format(stat.footerSubtextCount)} ${stat.title === "نرخ تبدیل" ? "بازدید" : "سفارش"}`;
return (
<Card key={stat.title} className="@container/card shadow-none">
<CardHeader className="flex items-start justify-between gap-2">
<div>
<CardDescription className="font-medium">
{stat.title}
</CardDescription>
<CardTitle className="text-xl font-bold @[600px]/card:text-xl @[800px]/card:text-2xll">
{formattedValue}
</CardTitle>
</div>
<span
className={cn(
"inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs font-medium",
trendClass,
)}
>
<Icon className="size-3" aria-hidden="true" />
{formattedTrend}
</span>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex items-center gap-1.5 font-medium">
<span
className={
isNeutral
? ""
: isPositive
? "text-success"
: "text-destructive"
}
>
{footerLabel}
</span>
<Icon
className={cn(
"size-3.5",
isNeutral
? "text-muted-foreground"
: isPositive
? "text-success"
: "text-destructive",
)}
aria-hidden="true"
/>
</div>
<div className="text-xs text-muted-foreground">
{footerSubtext}
</div>
</CardFooter>
</Card>
);
})}
</div>
);
};
function ScatterTooltip({ active, payload }: any) {
if (!active || !payload?.length) return null;
const data = payload[0]?.payload as DailyMetric | undefined;
if (!data) return null;
return (
<div className="rounded-lg border border-border bg-popover px-3 py-2 shadow-lg">
<p className="mb-1.5 text-xs font-medium text-foreground">{data.day}</p>
<div className="space-y-1">
<div className="flex items-center justify-between gap-4">
<span className={tooltipLabelClass}>سفارش‌ها:</span>
<span className={tooltipValueClass}>
{numberFormatter.format(data.orderCount)}
</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className={tooltipLabelClass}>میانگین سفارش:</span>
<span className={tooltipValueClass}>
{currencyFormatter.format(data.avgOrderValue)}
</span>
</div>
<div className="flex items-center justify-between gap-4 border-t border-border pt-1">
<span className={tooltipLabelClass}>درآمد:</span>
<span className={tooltipValueClass}>
{currencyFormatter.format(data.totalRevenue)}
</span>
</div>
</div>
</div>
);
}
const AOVScatterChart = () => {
const totalOrders = dailyMetrics.reduce((s, d) => s + d.orderCount, 0);
const totalRevenue = dailyMetrics.reduce((s, d) => s + d.totalRevenue, 0);
const avgAOV = totalOrders === 0 ? 0 : totalRevenue / totalOrders;
return (
<div className="flex min-w-0 flex-1 flex-col rounded-xl border bg-card">
<div className={chartHeaderClass}>
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="icon"
className={chartIconBtnClass}
aria-label="میانگین سفارش در برابر حجم سفارش"
>
<BarChart3 className={chartIconClass} aria-hidden="true" />
</Button>
<h2 className={chartTitleClass}>میانگین سفارش در برابر حجم</h2>
</div>
<span className={tooltipLabelClass}>۳۰ روز اخیر</span>
</div>
<div className="flex flex-col gap-4 p-4 sm:gap-5 sm:p-5">
<div className="flex items-center gap-6">
<div className="flex flex-col gap-1">
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl">
{currencyFormatter.format(avgAOV)}
</p>
<p className="text-[10px] tracking-wider text-muted-foreground uppercase sm:text-xs">
میانگین سفارش
</p>
</div>
<div className="w-px self-stretch bg-border" />
<div className="flex flex-col gap-1">
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl">
{numberFormatter.format(totalOrders)}
</p>
<p className="text-[10px] tracking-wider text-muted-foreground uppercase sm:text-xs">
مجموع سفارش‌ها
</p>
</div>
<div className="w-px self-stretch bg-border" />
<div className="flex flex-col gap-1">
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl">
{compactCurrencyFormatter.format(totalRevenue)}
</p>
<p className="text-[10px] tracking-wider text-muted-foreground uppercase sm:text-xs">
درآمد کل
</p>
</div>
</div>
<div className="h-[240px] w-full min-w-0 sm:h-[280px]">
<ChartContainer
config={scatterChartConfig}
className="aspect-auto h-full w-full"
>
<ScatterChart margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
<CartesianGrid strokeDasharray="0" />
<XAxis
type="number"
dataKey="orderCount"
name="سفارش‌ها"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
/>
<YAxis
type="number"
dataKey="avgOrderValue"
name="میانگین سفارش"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
width={50}
tickFormatter={(v) => currencyFormatter.format(v)}
/>
<ZAxis
type="number"
dataKey="totalRevenue"
range={[40, 400]}
name="درآمد"
/>
<Tooltip
content={<ScatterTooltip />}
cursor={{ strokeDasharray: "3 3" }}
/>
<Scatter
data={dailyMetrics}
fill="var(--color-scatter)"
fillOpacity={0.6}
strokeWidth={0}
/>
</ScatterChart>
</ChartContainer>
</div>
</div>
</div>
);
};
function ChannelTooltip({
active,
payload,
label,
}: any) {
if (!active || !payload?.length) return null;
const total = payload.reduce(
(sum: number, entry: any) => sum + Number(entry.value ?? 0),
0,
);
return (
<div className="rounded-lg border border-border bg-popover px-3 py-2 shadow-lg">
<p className="mb-1.5 text-xs font-medium text-foreground">{label}</p>
<div className="space-y-1">
{payload.map((entry: any) => {
const dataKey = String(entry.dataKey ?? "");
const label =
channelChartConfig[dataKey as keyof typeof channelChartConfig]
?.label ?? entry.name ?? dataKey;
return (
<div key={`${entry.dataKey ?? entry.name ?? label}`} className="flex items-center gap-2">
<div
className="size-2 rounded-full"
style={{ backgroundColor: String(entry.color) }}
/>
<span className={tooltipLabelClass}>{label}:</span>
<span className={tooltipValueClass}>
{compactCurrencyFormatter.format(Number(entry.value))}
</span>
</div>
);
})}
<div className="border-t border-border pt-1">
<span className={tooltipValueClass}>
مجموع: {compactCurrencyFormatter.format(total)}
</span>
</div>
</div>
</div>
);
}
const ChannelRevenueChart = () => {
return (
<div className="flex min-w-0 flex-col rounded-xl border bg-card">
<div className={chartHeaderClass}>
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="icon"
className={chartIconBtnClass}
aria-label="درآمد بر اساس کانال"
>
<BarChart3 className={chartIconClass} aria-hidden="true" />
</Button>
<h2 className={chartTitleClass}>درآمد بر اساس کانال</h2>
</div>
</div>
<div className="flex flex-col gap-2 p-4 sm:p-5">
<div className="flex flex-wrap items-center gap-3 sm:gap-5">
{[
{ label: "آنلاین", color: palette.primary },
{ label: "حضوری", color: palette.secondary.light },
{ label: "عمده‌فروشی", color: palette.tertiary.light },
{ label: "مارکت‌پلیس", color: palette.quaternary.light },
].map((item) => (
<div key={item.label} className="flex items-center gap-1.5">
<div
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: item.color }}
/>
<span className={tooltipLabelClass}>{item.label}</span>
</div>
))}
</div>
<div className="h-[240px] w-full min-w-0 sm:h-[280px]">
<ChartContainer config={channelChartConfig} className="h-full w-full">
<BarChart
layout="vertical"
data={channelRevenueData}
barSize={24}
margin={{ top: 0, right: 0, bottom: 0, left: -9 }}
>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
tickFormatter={(v) => compactCurrencyFormatter.format(v)}
/>
<YAxis
type="category"
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
width={36}
/>
<Tooltip
content={<ChannelTooltip />}
cursor={{ fillOpacity: 0.05 }}
/>
<Bar
dataKey="online"
stackId="channel"
fill="var(--color-online)"
radius={[4, 0, 0, 4]}
/>
<Bar
dataKey="inStore"
stackId="channel"
fill="var(--color-inStore)"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="wholesale"
stackId="channel"
fill="var(--color-wholesale)"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="marketplace"
stackId="channel"
fill="var(--color-marketplace)"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ChartContainer>
</div>
</div>
</div>
);
};
function RevenueTooltip({
active,
payload,
label,
}: any) {
if (!active || !payload?.length) return null;
const visiblePayload = payload.filter(
(entry: any) => entry.dataKey !== "gapUp" && entry.dataKey !== "gapDown",
);
if (!visiblePayload.length) return null;
return (
<div className="rounded-lg border border-border bg-popover px-3 py-2 shadow-lg">
<p className="mb-1.5 text-xs font-medium text-foreground">{label}</p>
<div className="space-y-1">
{visiblePayload.map((entry: any) => {
const dataKey = String(entry.dataKey ?? "");
const cfgLabel =
revenueChartConfig[dataKey as keyof typeof revenueChartConfig]
?.label ?? dataKey;
return (
<div key={`${entry.dataKey ?? cfgLabel}`} className="flex items-center gap-2">
<div
className="size-2 rounded-full"
style={{ backgroundColor: String(entry.color) }}
/>
<span className={tooltipLabelClass}>{cfgLabel}:</span>
<span className={tooltipValueClass}>
{currencyFormatter.format(Math.abs(Number(entry.value)))}
</span>
</div>
);
})}
</div>
</div>
);
}
const RevenueOverviewChart = () => {
const totalThisYear = fullYearData.reduce(
(sum, entry) => sum + entry.thisYear,
0,
);
const totalLastYear = fullYearData.reduce(
(sum, entry) => sum + Math.abs(entry.lastYear),
0,
);
const changePercent =
totalLastYear === 0
? 0
: ((totalThisYear - totalLastYear) / totalLastYear) * 100;
const isPositive = changePercent >= 0;
return (
<div className="flex min-w-0 flex-1 flex-col rounded-xl border bg-card">
<div className={chartHeaderClass}>
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="icon"
className={chartIconBtnClass}
aria-label="نمای کلی درآمد"
>
<BarChart3 className={chartIconClass} aria-hidden="true" />
</Button>
<h2 className={chartTitleClass}>نمای کلی درآمد</h2>
</div>
<div className="flex items-center gap-3 sm:gap-5">
{[
{ label: "امسال", color: palette.primary },
{ label: "سال قبل", color: palette.secondary.light },
].map((item) => (
<div key={item.label} className="flex items-center gap-1.5">
<div
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: item.color }}
/>
<span className={tooltipLabelClass}>{item.label}</span>
</div>
))}
</div>
</div>
<div className="flex flex-col gap-4 p-4 sm:gap-5 sm:p-5">
<div className="flex flex-col gap-1">
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl">
{currencyFormatter.format(totalThisYear)}
</p>
<p
className={cn(
"text-[10px] tracking-wider uppercase sm:text-xs",
isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{isPositive ? "+" : "-"}
{Math.abs(changePercent).toLocaleString("fa-IR", {
maximumFractionDigits: 1,
})}
٪ نسبت به سال قبل
</p>
</div>
<div className="h-[240px] w-full min-w-0 sm:h-[280px]">
<ChartContainer config={revenueChartConfig} className="h-full w-full">
<BarChart data={fullYearData} stackOffset="sign">
<CartesianGrid vertical={false} strokeDasharray="0" />
<XAxis
dataKey="quarter"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
/>
<YAxis
orientation="right"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
width={50}
domain={[-revenueDomainMax, revenueDomainMax]}
tickCount={9}
tickFormatter={(v: number) =>
compactCurrencyFormatter.format(Math.abs(v))
}
/>
<Tooltip
content={<RevenueTooltip />}
cursor={{ fillOpacity: 0.05 }}
/>
{/* Invisible spacer pushes thisYear bars above zero line */}
<Bar
dataKey="gapUp"
stackId="revenue"
fill="transparent"
barSize={22}
isAnimationActive={false}
/>
<Bar
dataKey="thisYear"
stackId="revenue"
fill="var(--color-thisYear)"
radius={6}
barSize={22}
/>
{/* Invisible spacer pushes lastYear bars below zero line */}
<Bar
dataKey="gapDown"
stackId="revenue"
fill="transparent"
barSize={22}
isAnimationActive={false}
/>
<Bar
dataKey="lastYear"
stackId="revenue"
fill="var(--color-lastYear)"
fillOpacity={0.4}
radius={6}
barSize={22}
/>
</BarChart>
</ChartContainer>
</div>
</div>
</div>
);
};
const ActivityFeed = () => {
return (
<div className="flex flex-col rounded-xl border bg-card">
<div className={chartHeaderClass}>
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="icon"
className={chartIconBtnClass}
aria-label="فعالیت تیم‌های اخیر"
>
<Bell className={chartIconClass} aria-hidden="true" />
</Button>
<h2 className={chartTitleClass}>فعالیت تیم‌های اخیر</h2>
</div>
</div>
<ScrollArea className="h-[340px] sm:h-[370px]">
<div className="divide-y px-4 sm:px-5">
{activityEvents.map((event) => {
const Icon = activityIcons[event.type];
const colorClass = activityColors[event.type];
return (
<div key={event.id} className="flex items-start gap-3 py-3">
<div
className={cn(
"mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full",
colorClass,
)}
>
<Icon className="size-3.5" aria-hidden="true" />
</div>
<div className="min-w-0 flex-1">
<p className="text-xs text-foreground sm:text-sm">
{event.message}
</p>
<p className="mt-0.5 text-[10px] text-muted-foreground sm:text-xs">
{shortDateFormatter.format(event.timestamp)},{" "}
{timeFormatter.format(event.timestamp)}
</p>
</div>
</div>
);
})}
</div>
</ScrollArea>
</div>
);
};
const RecentOrdersTable = () => {
const [searchQuery, setSearchQuery] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState<OrderStatus | "all">(
"all",
);
const [currentPage, setCurrentPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(10);
const [isHydrated, setIsHydrated] = React.useState(false);
// Hydrate from URL on mount
React.useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
const q = params.get("q");
if (q) setSearchQuery(q);
const nextStatus = params.get("status");
if (
nextStatus &&
(nextStatus === "all" ||
orderStatuses.includes(nextStatus as OrderStatus))
) {
setStatusFilter(nextStatus as OrderStatus | "all");
}
const nextPage = Number(params.get("page"));
if (!Number.isNaN(nextPage) && nextPage > 0) {
setCurrentPage(nextPage);
}
const nextPageSize = Number(params.get("pageSize"));
if (PAGE_SIZE_OPTIONS.includes(nextPageSize)) {
setPageSize(nextPageSize);
}
setIsHydrated(true);
}, []);
const filteredOrders = React.useMemo(() => {
return orders.filter((order) => {
const matchesSearch =
order.customer.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.orderNumber.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus =
statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus;
});
}, [searchQuery, statusFilter]);
const totalPages = Math.max(1, Math.ceil(filteredOrders.length / pageSize));
const paginatedOrders = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return filteredOrders.slice(startIndex, startIndex + pageSize);
}, [filteredOrders, currentPage, pageSize]);
// Reset page when filters change
React.useEffect(() => {
setCurrentPage(1);
}, [searchQuery, statusFilter, pageSize]);
// Sync state to URL
React.useEffect(() => {
if (!isHydrated || typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
if (searchQuery) {
params.set("q", searchQuery);
} else {
params.delete("q");
}
if (statusFilter !== "all") {
params.set("status", statusFilter);
} else {
params.delete("status");
}
if (currentPage > 1) {
params.set("page", String(currentPage));
} else {
params.delete("page");
}
if (pageSize !== PAGE_SIZE_OPTIONS[0]) {
params.set("pageSize", String(pageSize));
} else {
params.delete("pageSize");
}
const nextQuery = params.toString();
const nextUrl = nextQuery
? `${window.location.pathname}?${nextQuery}`
: window.location.pathname;
window.history.replaceState(null, "", nextUrl);
}, [searchQuery, statusFilter, currentPage, pageSize, isHydrated]);
const goToPage = (page: number) => {
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
};
const startRow = filteredOrders.length ? (currentPage - 1) * pageSize + 1 : 0;
const endRow = Math.min(currentPage * pageSize, filteredOrders.length);
return (
<div className="rounded-xl border bg-card">
<div className="flex flex-col gap-3 px-4 pt-4 sm:flex-row sm:items-center sm:gap-4 sm:px-6">
<div className="flex items-center gap-2">
<h2 className={chartTitleClass}>سفارش‌های اخیر</h2>
<span className="ms-1 inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-[10px] font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset sm:text-xs dark:bg-gray-800/50 dark:text-gray-400 dark:ring-gray-400/20">
{filteredOrders.length.toLocaleString("fa-IR")}
</span>
</div>
<div className="flex flex-1 flex-wrap items-center gap-2 sm:justify-end">
<div className="relative flex-1 sm:flex-none">
<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-search"
inputMode="search"
autoComplete="off"
aria-label="جستجوی سفارش‌ها"
className="h-8 w-full ps-9 text-sm sm:h-9 sm:w-[200px]"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 sm:h-9 sm:gap-2"
aria-label="فیلتر بر اساس وضعیت"
>
<span className="text-xs sm:text-sm">
{statusFilter === "all"
? "همه وضعیت‌ها"
: orderStatusLabels[statusFilter]}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[180px]">
<DropdownMenuLabel>فیلتر بر اساس وضعیت</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={statusFilter === "all"}
onCheckedChange={() => setStatusFilter("all")}
>
همه وضعیت‌ها
</DropdownMenuCheckboxItem>
{orderStatuses.map((status) => (
<DropdownMenuCheckboxItem
key={status}
checked={statusFilter === status}
onCheckedChange={() => setStatusFilter(status)}
>
{orderStatusLabels[status]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="px-4 pt-3 pb-4 sm:px-6">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="text-xs font-medium text-muted-foreground sm:text-sm">
مرجع سفارش
</TableHead>
<TableHead className="text-xs font-medium text-muted-foreground sm:text-sm">
مشتری
</TableHead>
<TableHead className="hidden text-xs font-medium text-muted-foreground sm:table-cell sm:text-sm">
تاریخ
</TableHead>
<TableHead className="text-xs font-medium text-muted-foreground sm:text-sm">
مجموع
</TableHead>
<TableHead className="text-xs font-medium text-muted-foreground sm:text-sm">
وضعیت
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedOrders.length === 0 ? (
<TableRow>
<TableCell
colSpan={5}
className="h-20 text-center text-sm text-muted-foreground"
>
سفارشی یافت نشد.
</TableCell>
</TableRow>
) : (
paginatedOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="text-xs font-medium text-muted-foreground sm:text-sm">
{order.orderNumber}
</TableCell>
<TableCell className="text-xs text-foreground sm:text-sm">
{order.customer}
</TableCell>
<TableCell className="hidden text-xs text-muted-foreground sm:table-cell sm:text-sm">
{shortDateFormatter.format(new Date(order.date))}
</TableCell>
<TableCell className="text-xs text-foreground tabular-nums sm:text-sm">
{currencyFormatter.format(order.total)}
</TableCell>
<TableCell>
<span
className={cn(
"inline-flex items-center rounded-md px-2 py-1 text-[10px] font-medium sm:text-xs",
statusStyles[order.status],
)}
>
{orderStatusLabels[order.status]}
</span>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col items-center justify-between gap-3 border-t px-4 py-3 sm:flex-row sm:px-6">
<div className="flex items-center gap-2 text-xs text-muted-foreground sm:text-sm">
<span className="hidden sm:inline">ردیف در هر صفحه:</span>
<Select
value={pageSize.toString()}
onValueChange={(value) => setPageSize(Number(value))}
>
<SelectTrigger className="h-8 w-[70px]" aria-label="ردیف در هر صفحه">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((size) => (
<SelectItem key={size} value={size.toString()}>
{size.toLocaleString("fa-IR")}
</SelectItem>
))}
</SelectContent>
</Select>
<span>
{filteredOrders.length === 0
? "۰"
: `${startRow.toLocaleString("fa-IR")}-${endRow.toLocaleString("fa-IR")}`}{" "}
از {filteredOrders.length.toLocaleString("fa-IR")}
</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="size-7"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
aria-label="رفتن به صفحه قبل"
>
<ChevronLeft className="size-3.5 rtl:rotate-180" aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon"
className="size-7"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="رفتن به صفحه بعد"
>
<ChevronRight className="size-3.5 rtl:rotate-180" aria-hidden="true" />
</Button>
</div>
</div>
</div>
);
};
const DashboardContent = () => {
return (
<div
id="dashboard-main"
tabIndex={-1}
className="mx-auto w-full max-w-7xl flex-1 space-y-4 overflow-auto bg-background p-3 sm:space-y-6 sm:p-4 md:p-6"
>
<DashboardHeader />
<StatCards />
<div className="grid gap-4 sm:gap-6 lg:grid-cols-[3fr_2fr]">
<RevenueOverviewChart />
<AOVScatterChart />
</div>
<div className="grid gap-4 sm:gap-6 lg:grid-cols-[2fr_1fr]">
<ChannelRevenueChart />
<ActivityFeed />
</div>
<RecentOrdersTable />
</div>
);
};
const getDefaultActiveItem = (group: NavGroup): NavItem | null => {
return group.items.find((item) => item.isActive) || group.items[0] || null;
};
const Dashboard10 = ({ className }: { className?: string }) => {
const [activeGroupIndex, setActiveGroupIndex] = React.useState(0);
const activeGroup = shellData.navGroups[activeGroupIndex];
const [activeItem, setActiveItem] = React.useState<NavItem | null>(
getDefaultActiveItem(activeGroup),
);
// Update activeItem when group changes
React.useEffect(() => {
setActiveItem(getDefaultActiveItem(activeGroup));
}, [activeGroupIndex]);
return (
<div
dir="rtl"
lang="fa"
className={cn("flex min-h-svh flex-col bg-background", className)}
>
<a
href="#dashboard-main"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:start-4 focus:z-50 focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:text-foreground focus:ring-2 focus:ring-ring"
>
رفتن به محتوای اصلی
</a>
<header className="sticky top-0 z-50 bg-background">
<div className="border-b">
<div className="mx-auto flex h-14 w-full max-w-7xl items-center gap-4 px-4 lg:px-6">
<MobileNav
activeGroupIndex={activeGroupIndex}
onSelectGroup={setActiveGroupIndex}
/>
<a href="#" className="flex items-center gap-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-primary">
<img
src={shellData.logo.src}
alt={shellData.logo.alt}
className="size-6 text-primary-foreground invert dark:invert-0"
/>
</div>
<span className="font-semibold">{shellData.logo.title}</span>
</a>
<nav
className="ms-4 hidden items-center gap-1 md:flex"
aria-label="ناوبری اصلی"
>
{shellData.navGroups.map((group, index) => (
<NavDropdown
key={group.title}
group={group}
isActive={index === activeGroupIndex}
onSelect={() => setActiveGroupIndex(index)}
/>
))}
</nav>
<div className="ms-auto flex items-center gap-2">
<div className="relative hidden md:block">
<Search
className="absolute top-1/2 start-2.5 size-4 -translate-y-1/2 text-muted-foreground"
aria-hidden="true"
/>
<Input
type="search"
placeholder="جستجو…"
className="h-9 w-64 ps-8"
aria-label="جستجو"
/>
</div>
<Button
variant="ghost"
size="icon"
className="md:hidden"
aria-label="جستجو"
>
<Search className="size-5" aria-hidden="true" />
</Button>
<NavUser user={shellData.user} />
</div>
</div>
</div>
<SubNav
group={activeGroup}
activeItem={activeItem}
onSelectItem={setActiveItem}
/>
</header>
<DashboardContent />
</div>
);
};
export { Dashboard10 };