Files
reui-blocks/blocks/dashboard10.tsx
T

2081 lines
63 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 };