Files

1908 lines
59 KiB
TypeScript
Raw Permalink 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 {
ArrowUpRight,
BarChart3,
Bell,
Box,
ChevronLeft,
ChevronRight,
ChevronsUpDown,
CircleDollarSign,
ClipboardList,
CreditCard,
Download,
Filter,
Globe,
HelpCircle,
LayoutDashboard,
LogOut,
MessageSquare,
MoreHorizontal,
Package,
PieChartIcon,
Plus,
RotateCcw,
Search,
Settings,
ShoppingCart,
Truck,
User,
Users,
Wallet,
} from "lucide-react";
import * as React from "react";
import type { TooltipProps } from "recharts";
import {
Area,
AreaChart,
CartesianGrid,
Cell,
Pie,
PieChart,
Sector,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { PieSectorDataItem } from "recharts/types/polar/Pie";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
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 {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip as ShadTooltip,
TooltipContent as ShadTooltipContent,
TooltipProvider as ShadTooltipProvider,
TooltipTrigger as ShadTooltipTrigger,
} from "@/components/ui/tooltip";
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[];
defaultOpen?: boolean;
};
type UserData = {
name: string;
email: string;
avatar: string;
};
type SidebarData = {
logo: {
src: string;
alt: string;
title: string;
description: string;
};
navGroups: NavGroup[];
footerGroup: NavGroup;
user?: UserData;
};
type StatItem = {
title: string;
previousValue: number;
value: number;
changePercent: number;
isPositive: boolean;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
format: "currency" | "number";
};
type SalesCategoryItem = {
name: string;
value: number;
percent: number;
color: string;
};
type RevenueFlowColors = {
thisYear: string;
prevYear: string;
};
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
type Order = {
id: string;
orderNumber: string;
customer: string;
customerInitials: string;
products: string[];
productCount: number;
status: OrderStatus;
total: number;
date: string;
};
type ActivityItem = {
title: string;
detail: string;
time: string;
};
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
});
const numberFormatter = new Intl.NumberFormat("fa-IR");
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
notation: "compact",
maximumFractionDigits: 0,
});
const monthLabel = (monthIndex: number) =>
new Intl.DateTimeFormat("fa-IR", { month: "short" }).format(
new Date(2025, monthIndex, 1),
);
const orderStatusLabels: Record<OrderStatus, string> = {
Processing: "در حال پردازش",
Shipped: "ارسال‌شده",
Delivered: "تحویل‌شده",
Cancelled: "لغوشده",
};
/**
* Custom hook for hover highlight interaction.
* Provides stable callback to prevent unnecessary re-renders in chart components.
*/
function useHoverHighlight<T extends string | number>() {
const [active, setActive] = React.useState<T | null>(null);
const handleHover = React.useCallback((value: T | null) => {
setActive(value);
}, []);
return { active, handleHover };
}
const mixBase = "var(--background)";
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 salesCategoryChartConfig = {
electronics: { label: "الکترونیک", color: palette.primary },
accessories: { label: "لوازم جانبی", theme: palette.secondary },
software: { label: "نرم‌افزار", theme: palette.tertiary },
other: { label: "سایر", theme: palette.quaternary },
} satisfies ChartConfig;
const revenueFlowChartConfig = {
thisYear: { label: "امسال", color: palette.primary },
prevYear: { label: "سال قبل", theme: palette.secondary },
} satisfies ChartConfig;
const sidebarData: SidebarData = {
logo: {
src: "/block-media/shadcnblocks/block/logos/NovaSales-logo.svg",
alt: "فروشگاه آکمه",
title: "ری یو آی Store",
description: "فروشگاه آنلاین",
},
navGroups: [
{
title: "اصلی",
defaultOpen: true,
items: [
{
label: "داشبورد",
icon: LayoutDashboard,
href: "#",
isActive: true,
},
{ label: "سفارش‌ها", icon: ClipboardList, href: "#" },
{ label: "مرجوعی‌ها", icon: RotateCcw, href: "#" },
],
},
{
title: "کاتالوگ",
defaultOpen: true,
items: [
{
label: "محصولات",
icon: Box,
href: "#",
children: [
{ label: "همه محصولات", icon: Package, href: "#" },
{ label: "دسته‌بندی‌ها", icon: Package, href: "#" },
{ label: "موجودی", icon: Package, href: "#" },
],
},
{ label: "ارسال", icon: Truck, href: "#" },
],
},
{
title: "مشتریان",
defaultOpen: false,
items: [
{ label: "همه مشتریان", icon: Users, href: "#" },
{ label: "پیام‌ها", icon: MessageSquare, href: "#" },
],
},
{
title: "تحلیل فروش",
defaultOpen: false,
items: [
{ label: "نمای کلی", icon: Globe, href: "#" },
{ label: "گزارش‌ها", icon: BarChart3, href: "#" },
{ label: "مالی", icon: Wallet, href: "#" },
],
},
],
footerGroup: {
title: "تنظیمات",
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
},
user: {
name: "علی رضایی",
email: "john@acme.store",
avatar:
"/block-media/shadcnblocks/block/avatar-2.webp",
},
};
const statsData: StatItem[] = [
{
title: "درآمد ماهانه",
previousValue: 148230,
value: 189540.75,
changePercent: 27.86,
isPositive: true,
icon: CircleDollarSign,
format: "currency",
},
{
title: "سفارش‌های تکمیل‌شده",
previousValue: 18452,
value: 21847,
changePercent: 18.4,
isPositive: true,
icon: ClipboardList,
format: "number",
},
{
title: "مشتریان جدید",
previousValue: 4120,
value: 4975,
changePercent: 20.8,
isPositive: true,
icon: Users,
format: "number",
},
{
title: "بازپرداخت‌ها",
previousValue: 9821,
value: 8473,
changePercent: 13.73,
isPositive: false,
icon: CreditCard,
format: "currency",
},
];
const fullYearData = [
{ month: monthLabel(0), thisYear: 42000, prevYear: 38000 },
{ month: monthLabel(1), thisYear: 38000, prevYear: 45000 },
{ month: monthLabel(2), thisYear: 52000, prevYear: 41000 },
{ month: monthLabel(3), thisYear: 45000, prevYear: 48000 },
{ month: monthLabel(4), thisYear: 58000, prevYear: 44000 },
{ month: monthLabel(5), thisYear: 41000, prevYear: 52000 },
{ month: monthLabel(6), thisYear: 55000, prevYear: 47000 },
{ month: monthLabel(7), thisYear: 48000, prevYear: 53000 },
{ month: monthLabel(8), thisYear: 62000, prevYear: 49000 },
{ month: monthLabel(9), thisYear: 54000, prevYear: 58000 },
{ month: monthLabel(10), thisYear: 67000, prevYear: 52000 },
{ month: monthLabel(11), thisYear: 71000, prevYear: 61000 },
];
type TimePeriod = "6months" | "year";
const periodLabels: Record<TimePeriod, string> = {
"6months": "۶ ماه اخیر",
year: "۱۲ ماه",
};
function getDataForPeriod(period: TimePeriod) {
if (period === "6months") return fullYearData.slice(0, 6);
return fullYearData;
}
const orderStatIRRata = {
total: 1247,
processing: { count: 156, percent: 12.5 },
shipped: { count: 423, percent: 33.9 },
delivered: { count: 668, percent: 53.6 },
};
const salesCategoryData: SalesCategoryItem[] = [
{
name: "الکترونیک",
value: 145200,
percent: 58,
color: palette.primary,
},
{
name: "لوازم جانبی",
value: 62400,
percent: 25,
color: `color-mix(in oklch, var(--primary) 80%, ${mixBase})`,
},
{
name: "نرم‌افزار",
value: 32500,
percent: 13,
color: `color-mix(in oklch, var(--primary) 60%, ${mixBase})`,
},
{
name: "سایر",
value: 10000,
percent: 4,
color: `color-mix(in oklch, var(--primary) 42%, ${mixBase})`,
},
];
const orderStatuses: OrderStatus[] = [
"Processing",
"Shipped",
"Delivered",
"Cancelled",
];
const orders: Order[] = [
{
id: "1",
orderNumber: "ORD-2024-001",
customer: "Sarah Johnson",
customerInitials: "SJ",
products: ["MacBook Pro 14", "Magic Mouse"],
productCount: 2,
status: "Delivered",
total: 2499.0,
date: "Jan 28, 2024",
},
{
id: "2",
orderNumber: "ORD-2024-002",
customer: "Michael Chen",
customerInitials: "MC",
products: ["iPhone 15 Pro", "AirPods Pro"],
productCount: 2,
status: "Shipped",
total: 1348.0,
date: "Jan 27, 2024",
},
{
id: "3",
orderNumber: "ORD-2024-003",
customer: "Emma Wilson",
customerInitials: "EW",
products: ["iPad Air", "Apple Pencil", "Smart Keyboard"],
productCount: 3,
status: "Processing",
total: 1198.0,
date: "Jan 27, 2024",
},
{
id: "4",
orderNumber: "ORD-2024-004",
customer: "James Rodriguez",
customerInitials: "JR",
products: ["Apple Watch Ultra"],
productCount: 1,
status: "Delivered",
total: 799.0,
date: "Jan 26, 2024",
},
{
id: "5",
orderNumber: "ORD-2024-005",
customer: "Lisa Park",
customerInitials: "LP",
products: ["Mac Mini M2"],
productCount: 1,
status: "Cancelled",
total: 599.0,
date: "Jan 26, 2024",
},
{
id: "6",
orderNumber: "ORD-2024-006",
customer: "David Kim",
customerInitials: "DK",
products: ["Studio Display", "Mac Studio"],
productCount: 2,
status: "Shipped",
total: 5498.0,
date: "Jan 25, 2024",
},
{
id: "7",
orderNumber: "ORD-2024-007",
customer: "Anna Martinez",
customerInitials: "AM",
products: ["MacBook Air M2"],
productCount: 1,
status: "Delivered",
total: 1199.0,
date: "Jan 25, 2024",
},
{
id: "8",
orderNumber: "ORD-2024-008",
customer: "Robert Taylor",
customerInitials: "RT",
products: ["iPhone 15", "MagSafe Charger"],
productCount: 2,
status: "Processing",
total: 878.0,
date: "Jan 24, 2024",
},
{
id: "9",
orderNumber: "ORD-2024-009",
customer: "Jennifer Lee",
customerInitials: "JL",
products: ["AirPods Max"],
productCount: 1,
status: "Shipped",
total: 549.0,
date: "Jan 24, 2024",
},
{
id: "10",
orderNumber: "ORD-2024-010",
customer: "William Brown",
customerInitials: "WB",
products: ["iPad Pro 12.9", "Magic Keyboard"],
productCount: 2,
status: "Delivered",
total: 1648.0,
date: "Jan 23, 2024",
},
{
id: "11",
orderNumber: "ORD-2024-011",
customer: "Sophia Davis",
customerInitials: "SD",
products: ["MacBook Pro 16"],
productCount: 1,
status: "Processing",
total: 2499.0,
date: "Jan 23, 2024",
},
{
id: "12",
orderNumber: "ORD-2024-012",
customer: "Daniel Garcia",
customerInitials: "DG",
products: ["Apple TV 4K", "HomePod mini"],
productCount: 2,
status: "Cancelled",
total: 278.0,
date: "Jan 22, 2024",
},
{
id: "13",
orderNumber: "ORD-2024-013",
customer: "Olivia White",
customerInitials: "OW",
products: ["iPhone 15 Pro Max"],
productCount: 1,
status: "Delivered",
total: 1199.0,
date: "Jan 22, 2024",
},
{
id: "14",
orderNumber: "ORD-2024-014",
customer: "Thomas Anderson",
customerInitials: "TA",
products: ["Mac Pro", "Pro Display XDR"],
productCount: 2,
status: "Shipped",
total: 12498.0,
date: "Jan 21, 2024",
},
{
id: "15",
orderNumber: "ORD-2024-015",
customer: "Emily Thompson",
customerInitials: "ET",
products: ["iPad mini", "Apple Pencil"],
productCount: 2,
status: "Delivered",
total: 648.0,
date: "Jan 21, 2024",
},
{
id: "16",
orderNumber: "ORD-2024-016",
customer: "Christopher Moore",
customerInitials: "CM",
products: ["AirPods Pro 2"],
productCount: 1,
status: "Processing",
total: 249.0,
date: "Jan 20, 2024",
},
{
id: "17",
orderNumber: "ORD-2024-017",
customer: "Isabella Jackson",
customerInitials: "IJ",
products: ["Apple Watch Series 9"],
productCount: 1,
status: "Shipped",
total: 399.0,
date: "Jan 20, 2024",
},
{
id: "18",
orderNumber: "ORD-2024-018",
customer: "Matthew Harris",
customerInitials: "MH",
products: ["MacBook Air 15"],
productCount: 1,
status: "Delivered",
total: 1299.0,
date: "Jan 19, 2024",
},
{
id: "19",
orderNumber: "ORD-2024-019",
customer: "Ava Clark",
customerInitials: "AC",
products: ["iPhone SE", "EarPods"],
productCount: 2,
status: "Cancelled",
total: 448.0,
date: "Jan 19, 2024",
},
{
id: "20",
orderNumber: "ORD-2024-020",
customer: "Joshua Lewis",
customerInitials: "JL",
products: ["Mac Mini M2 Pro"],
productCount: 1,
status: "Processing",
total: 1299.0,
date: "Jan 18, 2024",
},
];
const recentActivity: ActivityItem[] = [
{
title: "ثبت‌نام کاربر جدید",
detail: "الکس مورگان",
time: "۲ دقیقه پیش",
},
{
title: "ثبت سفارش جدید",
detail: "الکس مورگان",
time: "۲ دقیقه پیش",
},
{
title: "دریافت پرداخت",
detail: "الکس مورگان",
time: "۲ دقیقه پیش",
},
{
title: "ارسال فاکتور",
detail: "جیمی لی",
time: "۶ دقیقه پیش",
},
{
title: "ارتقای اشتراک",
detail: "نوح رابرتس",
time: "۱۲ دقیقه پیش",
},
{
title: "صدور بازپرداخت",
detail: "الیویا وایت",
time: "۱۸ دقیقه پیش",
},
{
title: "بازنشانی رمز",
detail: "ایتن پارکر",
time: "۲۴ دقیقه پیش",
},
{
title: "نظر جدید",
detail: "سوفیا دیویس",
time: "۳۲ دقیقه پیش",
},
{
title: "بستن تیکت پشتیبانی",
detail: "مایکل چن",
time: "۴۵ دقیقه پیش",
},
{
title: "پردازش تسویه",
detail: "اما ویلسون",
time: "۱ ساعت پیش",
},
];
const SidebarLogo = ({ logo }: { logo: SidebarData["logo"] }) => {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" tooltip={logo.title}>
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-primary">
<img
src={logo.src}
alt={logo.alt}
width={24}
height={24}
className="size-6 text-primary-foreground invert dark:invert-0"
/>
</div>
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-medium">{logo.title}</span>
<span className="text-xs text-muted-foreground">
{logo.description}
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
};
const NavMenuItem = ({ item }: { item: NavItem }) => {
const Icon = item.icon;
const hasChildren = item.children && item.children.length > 0;
if (!hasChildren) {
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={item.isActive}
tooltip={item.label}
>
<a href={item.href}>
<Icon className="size-4" />
<span>{item.label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
return (
<Collapsible asChild defaultOpen className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton isActive={item.isActive} tooltip={item.label}>
<Icon className="size-4" />
<span>{item.label}</span>
<ChevronRight className="ms-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children!.map((child) => (
<SidebarMenuSubItem key={child.label}>
<SidebarMenuSubButton asChild isActive={child.isActive}>
<a href={child.href}>{child.label}</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
};
const NavUser = ({ user }: { user: UserData }) => {
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="size-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">
{user.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-start text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
<ChevronsUpDown className="ms-auto size-4" aria-hidden="true" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<Avatar className="size-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">
{user.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-start text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</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>
</SidebarMenuItem>
</SidebarMenu>
);
};
const AppSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
return (
<Sidebar side="right" variant="inset" collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 group-data-[collapsible=icon]:flex-col">
<SidebarLogo logo={sidebarData.logo} />
<SidebarTrigger className="ms-auto group-data-[collapsible=icon]:ms-0" />
</div>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((group) => (
<SidebarGroup key={group.title}>
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => (
<NavMenuItem key={item.label} item={item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<SidebarFooter>
{sidebarData.user && <NavUser user={sidebarData.user} />}
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
};
const DashboardHeader = () => {
return (
<header className="flex w-full items-center gap-3 border-b bg-background px-4 py-4 sm:px-6">
<LayoutDashboard className="size-5" aria-hidden="true" />
<h1 className="text-base font-medium">داشبورد</h1>
<div className="ms-auto flex items-center gap-2">
<div className="relative w-full max-w-[220px] sm:max-w-[260px]">
<Search
className="absolute top-1/2 start-3 size-4 -translate-y-1/2 text-muted-foreground"
aria-hidden="true"
/>
<Input
type="search"
name="header-search"
inputMode="search"
autoComplete="off"
aria-label="جستجوی داشبورد"
placeholder="جستجوی سفارش‌ها، مشتریان…"
className="h-9 w-full pe-14 ps-9 text-sm"
/>
<kbd className="pointer-events-none absolute top-1/2 end-2 hidden -translate-y-1/2 rounded-md border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline-flex">
{"\u2318"}
{"\u00a0"}K
</kbd>
</div>
<Button
variant="outline"
size="icon"
className="size-9"
aria-label="اعلان‌ها"
>
<Bell className="size-4" />
</Button>
<Button
variant="outline"
size="icon"
className="size-9"
aria-label="راهنما"
>
<HelpCircle className="size-4" />
</Button>
</div>
</header>
);
};
const WelcomeSection = () => {
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between sm:gap-6">
<div className="space-y-2 sm:space-y-5">
<h2 className="text-lg leading-relaxed font-semibold sm:text-[22px]">
خوش برگشتید، علی!
</h2>
<p className="text-sm text-muted-foreground sm:text-base">
امروز{" "}
<span className="font-medium text-foreground">۱۲ سفارش</span> برای
ارسال و{" "}
<span className="font-medium text-foreground">۳ مرجوعی</span> در
انتظار دارید
</p>
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<Button
variant="outline"
size="sm"
className="h-8 gap-2 sm:h-9"
aria-label="خروجی"
>
<Download className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">خروجی</span>
</Button>
<Button size="sm" className="h-8 gap-2 sm:h-9" aria-label="افزودن محصول">
<Plus className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">افزودن محصول</span>
</Button>
</div>
</div>
);
};
const StatsCards = () => {
return (
<div className="rounded-xl border bg-card">
<div className="grid grid-cols-1 divide-x-0 divide-y divide-border sm:grid-cols-2 sm:divide-y-0 lg:grid-cols-4 lg:divide-x">
{statsData.map((stat) => {
const formatter =
stat.format === "currency" ? currencyFormatter : numberFormatter;
return (
<div key={stat.title} className="space-y-4 p-4 sm:p-6">
<div className="flex items-center gap-1.5 text-muted-foreground">
<stat.icon
className="size-4 sm:size-[18px]"
aria-hidden="true"
/>
<span className="text-xs font-medium sm:text-sm">
{stat.title}
</span>
</div>
<p className="text-xl font-semibold tracking-tight sm:text-[28px]">
{formatter.format(stat.value)}
</p>
<div className="flex flex-wrap items-center gap-2 text-[10px] sm:text-xs xl:flex-nowrap">
<span
className={cn(
"font-medium",
stat.isPositive
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400",
)}
>
{stat.isPositive ? "+" : "-"}
{stat.changePercent.toLocaleString("fa-IR", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
٪
<span className="hidden sm:inline">
(
{formatter.format(
Math.abs(stat.value - stat.previousValue),
)}
)
</span>
</span>
<span className="hidden items-center gap-2 text-muted-foreground sm:inline-flex">
<span className="size-1 rounded-full bg-muted-foreground" />
<span className="xl:whitespace-nowrap">نسبت به ماه قبل</span>
</span>
</div>
</div>
);
})}
</div>
</div>
);
};
const OrderStatusChart = () => {
const { active: activeSegment, handleHover } = useHoverHighlight<number>();
const orderStatusItems = [
{
key: "processing",
label: orderStatusLabels.Processing,
...orderStatIRRata.processing,
color: palette.primary,
},
{
key: "shipped",
label: orderStatusLabels.Shipped,
...orderStatIRRata.shipped,
color: palette.secondary.light,
},
{
key: "delivered",
label: orderStatusLabels.Delivered,
...orderStatIRRata.delivered,
color: palette.tertiary.light,
},
];
return (
<div className="flex flex-col gap-4 rounded-xl border bg-card p-4 sm:p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 sm:gap-2.5">
<Button
variant="outline"
size="icon"
className="size-7 sm:size-8"
aria-label="وضعیت سفارش‌ها"
>
<ShoppingCart className="size-4 text-muted-foreground sm:size-[18px]" />
</Button>
<div>
<span className="text-sm font-medium sm:text-base">
وضعیت سفارش‌ها
</span>
<p className="text-[10px] text-muted-foreground sm:text-xs">
{numberFormatter.format(orderStatIRRata.total)} سفارش این ماه
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 sm:size-8"
aria-label="گزینه‌های بیشتر"
>
<MoreHorizontal className="size-4 text-muted-foreground" />
</Button>
</div>
<div className="space-y-3">
<div className="flex h-3 w-full overflow-hidden rounded-full sm:h-4">
{orderStatusItems.map((item, index) => (
<ShadTooltipProvider key={item.key}>
<ShadTooltip>
<ShadTooltipTrigger asChild>
<button
type="button"
className={cn(
"h-full border-0 p-0 transition-opacity duration-200 motion-reduce:transition-none",
activeSegment !== null &&
activeSegment !== index &&
"opacity-40",
)}
style={{
width: `${item.percent}%`,
backgroundColor: item.color,
}}
onPointerEnter={() => handleHover(index)}
onPointerLeave={() => handleHover(null)}
onFocus={() => handleHover(index)}
onBlur={() => handleHover(null)}
aria-label={`${item.label}: ${numberFormatter.format(item.count)} سفارش (${item.percent.toLocaleString("fa-IR")}٪)`}
/>
</ShadTooltipTrigger>
<ShadTooltipContent
side="top"
sideOffset={8}
className="px-3 py-2"
>
<div className="grid gap-1">
<div className="flex items-center gap-2">
<span
className="size-2 rounded-full"
style={{ backgroundColor: item.color }}
/>
<span className="font-medium">{item.label}</span>
<span className="text-muted-foreground tabular-nums">
{item.percent.toLocaleString("fa-IR")}٪
</span>
</div>
<span className="text-muted-foreground tabular-nums">
{numberFormatter.format(item.count)} سفارش
</span>
</div>
</ShadTooltipContent>
</ShadTooltip>
</ShadTooltipProvider>
))}
</div>
<div className="flex items-center justify-between text-[10px] sm:text-xs">
{orderStatusItems.map((item, index) => (
<span
key={item.key}
className={cn(
"text-muted-foreground tabular-nums transition-opacity duration-200 motion-reduce:transition-none",
activeSegment !== null &&
activeSegment !== index &&
"opacity-40",
)}
>
{item.percent.toLocaleString("fa-IR")}٪
</span>
))}
</div>
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
{orderStatusItems.map((item, index) => (
<ShadTooltipProvider key={item.key}>
<ShadTooltip>
<ShadTooltipTrigger asChild>
<button
type="button"
className={cn(
"flex items-center gap-1.5 border-0 bg-transparent p-0 transition-opacity duration-200 motion-reduce:transition-none",
activeSegment !== null &&
activeSegment !== index &&
"opacity-40",
)}
onPointerEnter={() => handleHover(index)}
onPointerLeave={() => handleHover(null)}
onFocus={() => handleHover(index)}
onBlur={() => handleHover(null)}
>
<span
className="size-2.5 rounded-full sm:size-3"
style={{ backgroundColor: item.color }}
/>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{item.label}
</span>
</button>
</ShadTooltipTrigger>
<ShadTooltipContent
side="top"
sideOffset={8}
className="px-3 py-2"
>
<div className="grid gap-1">
<div className="flex items-center gap-2">
<span
className="size-2 rounded-full"
style={{ backgroundColor: item.color }}
/>
<span className="font-medium">{item.label}</span>
<span className="text-muted-foreground tabular-nums">
{item.percent.toLocaleString("fa-IR")}٪
</span>
</div>
<span className="text-muted-foreground tabular-nums">
{numberFormatter.format(item.count)} سفارش
</span>
</div>
</ShadTooltipContent>
</ShadTooltip>
</ShadTooltipProvider>
))}
</div>
</div>
</div>
);
};
const renderActiveShape = (props: PieSectorDataItem) => {
const {
cx = 0,
cy = 0,
innerRadius = 0,
outerRadius = 0,
startAngle = 0,
endAngle = 0,
fill = "currentColor",
} = props;
return (
<g>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius + 6}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
</g>
);
};
const SalesByCategoryChart = () => {
const { active: activeSlice, handleHover: setHoveredSlice } =
useHoverHighlight<number>();
const totalSales = salesCategoryData.reduce(
(acc, item) => acc + item.value,
0,
);
return (
<div className="flex flex-1 flex-col gap-4 rounded-xl border bg-card p-4 sm:p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 sm:gap-2.5">
<Button
variant="outline"
size="icon"
className="size-7 sm:size-8"
aria-label="فروش بر اساس دسته"
>
<PieChartIcon className="size-4 text-muted-foreground sm:size-[18px]" />
</Button>
<div>
<span className="text-sm font-medium sm:text-base">
فروش بر اساس دسته
</span>
<p className="flex items-center gap-1 text-[10px] text-muted-foreground sm:text-xs">
<ArrowUpRight
className="size-3 text-emerald-600"
aria-hidden="true"
/>
<span className="text-emerald-600">+۸.۴٪</span>
<span>نسبت به ماه قبل</span>
</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 sm:size-8"
aria-label="گزینه‌های بیشتر"
>
<MoreHorizontal className="size-4 text-muted-foreground" />
</Button>
</div>
<div className="flex flex-1 items-center gap-4 sm:gap-6">
<div className="relative size-[100px] shrink-0 sm:size-[120px]">
<ChartContainer
config={salesCategoryChartConfig}
className="h-full w-full"
>
<PieChart>
<Pie
data={salesCategoryData}
cx="50%"
cy="50%"
innerRadius="55%"
outerRadius="90%"
paddingAngle={2}
dataKey="value"
strokeWidth={0}
{...({ activeIndex: activeSlice ?? undefined } as any)}
activeShape={renderActiveShape}
onMouseEnter={(_: unknown, index: number) =>
setHoveredSlice(index)
}
onMouseLeave={() => setHoveredSlice(null)}
>
{salesCategoryData.map((entry) => (
<Cell key={entry.name} fill={entry.color} />
))}
</Pie>
</PieChart>
</ChartContainer>
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
<span className="text-sm font-semibold sm:text-base">
{compactCurrencyFormatter.format(totalSales)}
</span>
<span className="text-[8px] text-muted-foreground sm:text-[10px]">
مجموع
</span>
</div>
</div>
<div className="flex flex-1 flex-col gap-2 sm:gap-3">
{salesCategoryData.map((item, index) => (
<div
key={item.name}
className={cn(
"flex items-center justify-between gap-2 transition-opacity duration-200 motion-reduce:transition-none",
activeSlice !== null && activeSlice !== index && "opacity-50",
)}
onMouseEnter={() => setHoveredSlice(index)}
onMouseLeave={() => setHoveredSlice(null)}
>
<div className="flex items-center gap-2">
<div
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: item.color }}
/>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{item.name}
</span>
</div>
<div className="flex items-center gap-2 text-[10px] sm:text-xs">
<span className="font-medium tabular-nums">
{compactCurrencyFormatter.format(item.value)}
</span>
<span className="text-muted-foreground tabular-nums">
{item.percent.toLocaleString("fa-IR")}٪
</span>
</div>
</div>
))}
</div>
</div>
</div>
);
};
const SideChartsSection = () => {
return (
<div className="flex w-full flex-col gap-4 xl:w-[410px]">
<OrderStatusChart />
<SalesByCategoryChart />
</div>
);
};
function CustomTooltip({
active,
payload,
label,
colors,
}: any & { colors: RevenueFlowColors }) {
if (!active || !payload?.length) return null;
const thisYear = payload.find((p: any) => p.dataKey === "thisYear")?.value || 0;
const prevYear = payload.find((p: any) => p.dataKey === "prevYear")?.value || 0;
const diff = Number(thisYear) - Number(prevYear);
const percentage = prevYear ? Math.round((diff / Number(prevYear)) * 100) : 0;
const currentYear = new Date().getFullYear();
return (
<div className="rounded-lg border border-border bg-popover p-2 shadow-lg sm:p-3">
<p className="mb-1.5 text-xs font-medium text-foreground sm:mb-2 sm:text-sm">
{label}، {currentYear.toLocaleString("fa-IR")}
</p>
<div className="space-y-1 sm:space-y-1.5">
<div className="flex items-center gap-1.5 sm:gap-2">
<div
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: colors.thisYear }}
/>
<span className="text-[10px] text-muted-foreground sm:text-sm">
امسال:
</span>
<span className="text-[10px] font-medium text-foreground sm:text-sm">
{currencyFormatter.format(Number(thisYear))}
</span>
</div>
<div className="flex items-center gap-1.5 sm:gap-2">
<div
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: colors.prevYear }}
/>
<span className="text-[10px] text-muted-foreground sm:text-sm">
سال قبل:
</span>
<span className="text-[10px] font-medium text-foreground sm:text-sm">
{currencyFormatter.format(Number(prevYear))}
</span>
</div>
<div className="mt-1 border-t border-border pt-1">
<span
className={cn(
"text-[10px] font-medium sm:text-xs",
diff >= 0 ? "text-emerald-500" : "text-red-500",
)}
>
{diff >= 0 ? "+" : ""}
{percentage.toLocaleString("fa-IR")}٪ نسبت به سال قبل
</span>
</div>
</div>
</div>
);
}
const RevenueFlowChart = () => {
const [period, setPeriod] = React.useState<TimePeriod>("6months");
const { active: activeSeries, handleHover } = useHoverHighlight<
"thisYear" | "prevYear"
>();
const legendItems = [
{ key: "thisYear", label: "امسال", color: palette.primary },
{ key: "prevYear", label: "سال قبل", color: palette.secondary.light },
] as const;
React.useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
const nextPeriod = params.get("period");
if (nextPeriod === "6months" || nextPeriod === "year") {
setPeriod(nextPeriod);
}
}, []);
React.useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
if (period !== "6months") {
params.set("period", period);
} else {
params.delete("period");
}
const nextQuery = params.toString();
const nextUrl = nextQuery
? `${window.location.pathname}?${nextQuery}`
: window.location.pathname;
window.history.replaceState(null, "", nextUrl);
}, [period]);
const chartData = getDataForPeriod(period);
const totalRevenue = chartData.reduce((acc, item) => acc + item.thisYear, 0);
return (
<div className="flex min-w-0 flex-1 flex-col gap-4 rounded-xl border bg-card p-4 sm:gap-6 sm:p-6">
<div className="flex flex-wrap items-center gap-2 sm:gap-4">
<div className="flex flex-1 flex-col gap-1">
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl">
{currencyFormatter.format(totalRevenue)}
</p>
<p className="text-xs text-muted-foreground">
درآمد کل ({periodLabels[period]})
</p>
</div>
<div className="hidden items-center gap-3 sm:flex sm:gap-5">
{legendItems.map((item) => (
<div
key={item.key}
className={cn(
"flex items-center gap-1.5 transition-opacity duration-200 motion-reduce:transition-none",
activeSeries !== null &&
activeSeries !== item.key &&
"opacity-40",
)}
onMouseEnter={() => handleHover(item.key)}
onMouseLeave={() => handleHover(null)}
>
<div
className="size-2.5 rounded-full sm:size-3"
style={{ backgroundColor: item.color }}
/>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{item.label}
</span>
</div>
))}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 sm:size-8"
aria-label="انتخاب بازه زمانی"
>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>بازه زمانی</DropdownMenuLabel>
<DropdownMenuSeparator />
{(Object.keys(periodLabels) as TimePeriod[]).map((key) => (
<DropdownMenuCheckboxItem
key={key}
checked={period === key}
onCheckedChange={() => setPeriod(key)}
>
{periodLabels[key]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="h-[200px] w-full min-w-0 sm:h-[240px] lg:h-[280px]">
<ChartContainer
config={revenueFlowChartConfig}
className="h-full w-full"
>
<AreaChart data={chartData}>
<defs>
<linearGradient id="thisYearGradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--color-thisYear)"
stopOpacity={0.3}
/>
<stop
offset="100%"
stopColor="var(--color-thisYear)"
stopOpacity={0.05}
/>
</linearGradient>
<linearGradient id="prevYearGradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--color-prevYear)"
stopOpacity={0.2}
/>
<stop
offset="100%"
stopColor="var(--color-prevYear)"
stopOpacity={0.02}
/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="0" vertical={false} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dy={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dx={-5}
tickFormatter={(value) => compactCurrencyFormatter.format(value)}
width={40}
/>
<Tooltip
content={
<CustomTooltip
colors={{
thisYear: "var(--color-thisYear)",
prevYear: "var(--color-prevYear)",
}}
/>
}
cursor={{ strokeOpacity: 0.2 }}
/>
<Area
type="monotone"
dataKey="thisYear"
stroke="var(--color-thisYear)"
strokeWidth={activeSeries === "thisYear" ? 3 : 2}
fill="url(#thisYearGradient)"
fillOpacity={
activeSeries === null || activeSeries === "thisYear" ? 1 : 0.3
}
strokeOpacity={
activeSeries === null || activeSeries === "thisYear" ? 1 : 0.3
}
/>
<Area
type="monotone"
dataKey="prevYear"
stroke="var(--color-prevYear)"
strokeWidth={activeSeries === "prevYear" ? 3 : 2}
fill="url(#prevYearGradient)"
fillOpacity={
activeSeries === null || activeSeries === "prevYear" ? 1 : 0.3
}
strokeOpacity={
activeSeries === null || activeSeries === "prevYear" ? 1 : 0.3
}
/>
</AreaChart>
</ChartContainer>
</div>
</div>
);
};
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 RecentTransactionsTable = ({ className }: { className?: string }) => {
const [statusFilter, setStatusFilter] = React.useState<OrderStatus | "all">(
"all",
);
const [currentPage, setCurrentPage] = React.useState(1);
const [isHydrated, setIsHydrated] = React.useState(false);
const pageSize = 6;
React.useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
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);
}
setIsHydrated(true);
}, []);
const filteredOrders = React.useMemo(() => {
if (statusFilter === "all") return orders;
return orders.filter((order) => order.status === statusFilter);
}, [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]);
React.useEffect(() => {
setCurrentPage(1);
}, [statusFilter]);
React.useEffect(() => {
if (!isHydrated || typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
if (statusFilter !== "all") {
params.set("status", statusFilter);
} else {
params.delete("status");
}
if (currentPage > 1) {
params.set("page", String(currentPage));
} else {
params.delete("page");
}
const nextQuery = params.toString();
const nextUrl = nextQuery
? `${window.location.pathname}?${nextQuery}`
: window.location.pathname;
window.history.replaceState(null, "", nextUrl);
}, [statusFilter, currentPage, 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={cn("rounded-xl border bg-card", className)}>
<div className="flex items-center justify-between gap-3 px-4 pt-4 sm:px-6">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="size-7 shrink-0 sm:size-8"
aria-label="تراکنش‌های اخیر"
>
<ClipboardList className="size-4 text-muted-foreground sm:size-[18px]" />
</Button>
<span className="text-sm font-medium sm:text-base">
تراکنش‌های اخیر
</span>
<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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 sm:h-9 sm:gap-2"
>
<Filter className="size-3.5 sm:size-4" aria-hidden="true" />
<span className="hidden sm:inline">فیلتر</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 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="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={4}
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-muted-foreground sm:text-sm">
{order.customer}
</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 items-center justify-between border-t px-4 py-3 text-[10px] text-muted-foreground sm:px-6 sm:text-xs">
<span>
{filteredOrders.length === 0
? "۰"
: `${startRow.toLocaleString("fa-IR")}-${endRow.toLocaleString("fa-IR")}`}{" "}
از {filteredOrders.length.toLocaleString("fa-IR")}
</span>
<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" />
</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" />
</Button>
</div>
</div>
</div>
);
};
const RecentActivity = ({ className }: { className?: string }) => {
return (
<div className={cn("rounded-xl border bg-card", className)}>
<div className="flex items-center justify-between gap-3 px-4 pt-4 sm:px-6">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="size-7 shrink-0 sm:size-8"
aria-label="فعالیت تیم‌های اخیر"
>
<Bell className="size-4 text-muted-foreground sm:size-[18px]" />
</Button>
<span className="text-sm font-medium sm:text-base">
فعالیت تیم‌های اخیر
</span>
</div>
</div>
<ScrollArea className="h-[360px] px-4 pt-2 pb-4 text-xs sm:px-6 sm:text-sm">
<div className="divide-y">
{recentActivity.map((item) => (
<div key={`${item.title}-${item.time}`} className="py-3">
<div className="flex items-center justify-between gap-3">
<span className="font-medium text-foreground">
{item.title}
</span>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{item.time}
</span>
</div>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{item.detail}
</span>
</div>
))}
</div>
</ScrollArea>
</div>
);
};
const DashboardContent = () => {
return (
<div
id="dashboard-main"
tabIndex={-1}
className="w-full flex-1 space-y-4 overflow-auto bg-background p-3 sm:space-y-6 sm:p-4 md:p-6"
>
<WelcomeSection />
<StatsCards />
<div className="flex flex-col gap-4 sm:gap-6 xl:flex-row">
<RevenueFlowChart />
<SideChartsSection />
</div>
<div className="flex flex-col gap-4 xl:flex-row">
<RecentTransactionsTable className="flex-1" />
<RecentActivity className="xl:w-[360px]" />
</div>
</div>
);
};
const Dashboard5 = ({ className }: { className?: string }) => {
return (
<ShadTooltipProvider>
<SidebarProvider
dir="rtl"
lang="fa"
className={cn("min-h-svh w-full overflow-hidden bg-sidebar", 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>
<AppSidebar />
<SidebarInset className="min-h-svh min-w-0 w-full overflow-hidden md:peer-data-[variant=inset]:m-0 md:peer-data-[variant=inset]:rounded-none md:peer-data-[variant=inset]:shadow-none md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-0 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:me-0">
<DashboardHeader />
<DashboardContent />
</SidebarInset>
</SidebarProvider>
</ShadTooltipProvider>
);
};
export { Dashboard5 };