Files
reui-blocks/blocks/dashboard4.tsx
T

1851 lines
60 KiB
TypeScript
Raw Normal View History

"use client";
import {
ArrowDownRight,
ArrowUpRight,
BarChart3,
Bell,
Banknote,
Box,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
CircleDollarSign,
ClipboardList,
Copy,
Eye,
Filter,
Globe,
HelpCircle,
LayoutDashboard,
LayoutGrid,
LineChart,
LogOut,
MessageSquare,
MoreHorizontal,
Package,
Pencil,
RotateCcw,
Search,
Settings,
Truck,
User,
Users,
X,
} from "lucide-react";
import * as React from "react";
import type { TooltipProps } from "recharts";
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts";
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
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 Segment = {
name: string;
value: number;
color: string;
};
type CategoryBreakdownRow = {
name: string;
orderCount: number;
changePercent: number;
isPositive: boolean;
};
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
type Order = {
id: string;
orderNumber: string;
customer: string;
customerInitials: string;
category: string;
products: string[];
productCount: number;
status: OrderStatus;
total: number;
date: string;
};
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
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: "لغوشده",
};
const categoryLabels: Record<string, string> = {
"Computers & tablets": "رایانه و تبلت",
Mobile: "موبایل",
"Wearables & audio": "پوشیدنی و صوتی",
"Home & TV": "خانه و تلویزیون",
};
const formatOrderDate = (dateStr: string) => {
const parsed = new Date(dateStr);
if (Number.isNaN(parsed.getTime())) return dateStr;
return new Intl.DateTimeFormat("fa-IR", {
year: "numeric",
month: "short",
day: "numeric",
}).format(parsed);
};
/**
* 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 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: LineChart, href: "#" },
],
},
],
footerGroup: {
title: "تنظیمات",
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
},
user: {
name: "علی رضایی",
email: "john@acme.store",
avatar:
"/block-media/shadcnblocks/block/avatar-8.webp",
},
};
const statsData: StatItem[] = [
{
title: "فروش ناخالص (ماه جاری)",
previousValue: 148230,
value: 189541,
changePercent: 27.9,
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.7,
isPositive: false,
icon: Banknote,
format: "currency",
},
];
const segments: Segment[] = [
{ name: "رایانه و تبلت", value: 78000, color: palette.primary },
{ name: "موبایل", value: 52000, color: palette.secondary.light },
{ name: "پوشیدنی و صوتی", value: 35000, color: palette.tertiary.light },
{ name: "خانه و تلویزیون", value: 24541, color: palette.quaternary.light },
];
const categoryBreakdown: CategoryBreakdownRow[] = [
{
name: "رایانه و تبلت",
orderCount: 8420,
changePercent: 14.2,
isPositive: true,
},
{ name: "موبایل", orderCount: 5340, changePercent: 19.1, isPositive: true },
{
name: "پوشیدنی و صوتی",
orderCount: 2980,
changePercent: 7.4,
isPositive: true,
},
{
name: "خانه و تلویزیون",
orderCount: 1240,
changePercent: 4.2,
isPositive: false,
},
];
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 orderStatuses: OrderStatus[] = [
"Processing",
"Shipped",
"Delivered",
"Cancelled",
];
const orders: Order[] = [
{
id: "1",
orderNumber: "ORD-2024-001",
customer: "Sarah Johnson",
customerInitials: "SJ",
category: "Computers & tablets",
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",
category: "Mobile",
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",
category: "Computers & tablets",
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",
category: "Wearables & audio",
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",
category: "Computers & tablets",
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",
category: "Computers & tablets",
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",
category: "Computers & tablets",
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",
category: "Mobile",
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",
category: "Wearables & audio",
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",
category: "Computers & tablets",
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",
category: "Computers & tablets",
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",
category: "Home & TV",
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",
category: "Mobile",
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",
category: "Computers & tablets",
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",
category: "Computers & tablets",
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",
category: "Wearables & audio",
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",
category: "Wearables & audio",
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",
category: "Computers & tablets",
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",
category: "Mobile",
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",
category: "Computers & tablets",
products: ["Mac Mini M2 Pro"],
productCount: 1,
status: "Processing",
total: 1299.0,
date: "Jan 18, 2024",
},
];
const PAGE_SIZE_OPTIONS = [10, 25, 50];
const revenueChartConfig = {
thisYear: { label: "فروش ناخالص", color: palette.primary },
prevYear: { label: "سال قبل", theme: palette.secondary },
} satisfies ChartConfig;
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?.length ?? 0) > 0;
if (!hasChildren) {
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={item.isActive}
tooltip={item.label}
>
<a href={item.href}>
<Icon className="size-4" aria-hidden="true" />
<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" aria-hidden="true" />
<span>{item.label}</span>
<ChevronRight
className="ms-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180"
aria-hidden="true"
/>
</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 StatsCards = () => {
return (
<div className="grid gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-4 lg:gap-6">
{statsData.map((stat) => {
const formatter =
stat.format === "currency" ? currencyFormatter : numberFormatter;
return (
<div
key={stat.title}
className="flex flex-col gap-3 rounded-xl border bg-card p-4 sm:p-5 lg:p-6"
>
<div className="flex items-center gap-1.5 text-muted-foreground sm:gap-2">
<stat.icon className="size-3.5 sm:size-4" aria-hidden="true" />
<span className="truncate text-[10px] font-medium sm:text-xs lg:text-sm">
{stat.title}
</span>
</div>
<p className="hidden text-[10px] text-muted-foreground/70 sm:block sm:text-xs">
{formatter.format(stat.previousValue)} ماه قبل
</p>
<p className="text-xl leading-tight font-semibold tracking-tight sm:text-xl lg:text-[28px]">
{formatter.format(stat.value)}
</p>
<div className="flex flex-wrap items-center gap-x-1 gap-y-0.5 text-[10px] sm:text-xs">
{stat.isPositive ? (
<ArrowUpRight className="size-3 shrink-0 text-emerald-600 sm:size-3.5" />
) : (
<ArrowDownRight className="size-3 shrink-0 text-red-600 sm:size-3.5" />
)}
<span
className={cn(
"whitespace-nowrap",
stat.isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{stat.isPositive ? "+" : "-"}
{stat.changePercent.toLocaleString("fa-IR", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
٪
</span>
<span className="whitespace-nowrap text-muted-foreground">
نسبت به ماه قبل
</span>
</div>
</div>
);
})}
</div>
);
};
const SegmentationPanel = () => {
const { active: activeSegment, handleHover } = useHoverHighlight<string>();
const totalSegmentSales = segments.reduce((sum, s) => sum + s.value, 0);
return (
<div className="flex min-w-0 flex-col rounded-xl border bg-card xl:w-[410px]">
<div className="flex h-14 items-center px-4 sm:px-5">
<div className="flex items-center gap-2.5">
<Button
variant="outline"
size="icon"
className="size-7 sm:size-8"
aria-label="فروش بر اساس دسته"
>
<LayoutGrid
className="size-4 text-muted-foreground sm:size-[18px]"
aria-hidden="true"
/>
</Button>
<h2 className="text-sm font-medium text-pretty sm:text-base">
فروش بر اساس دسته
</h2>
</div>
</div>
<div className="flex flex-col gap-4 p-4 sm:p-5">
<div className="flex items-baseline gap-2">
<span className="text-xl font-semibold sm:text-3xl">
{compactCurrencyFormatter.format(totalSegmentSales)}
</span>
<span className="text-sm text-muted-foreground">فروش ناخالص ماه جاری</span>
</div>
<div className="flex h-2.5 gap-1 sm:h-3">
{segments.map((segment, index) => (
<ShadTooltipProvider key={segment.name}>
<ShadTooltip>
<ShadTooltipTrigger asChild>
<button
type="button"
className={cn(
"h-full border-0 p-0 transition-opacity duration-200 motion-reduce:transition-none",
index === 0 && "rounded-s-full",
index === segments.length - 1 && "rounded-e-full",
activeSegment !== null &&
activeSegment !== segment.name &&
"opacity-40",
)}
style={{
flex: segment.value,
backgroundColor: segment.color,
}}
onPointerEnter={() => handleHover(segment.name)}
onPointerLeave={() => handleHover(null)}
onFocus={() => handleHover(segment.name)}
onBlur={() => handleHover(null)}
aria-label={`${segment.name}: ${currencyFormatter.format(segment.value)} (${Math.round((segment.value / totalSegmentSales) * 100).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: segment.color }}
/>
<span className="font-medium">{segment.name}</span>
<span className="text-muted-foreground tabular-nums">
{Math.round(
(segment.value / totalSegmentSales) * 100,
).toLocaleString("fa-IR")}
٪
</span>
</div>
<span className="text-muted-foreground tabular-nums">
{currencyFormatter.format(segment.value)}
</span>
</div>
</ShadTooltipContent>
</ShadTooltip>
</ShadTooltipProvider>
))}
</div>
<div className="flex flex-wrap gap-3 sm:gap-5">
{segments.map((segment) => (
<ShadTooltipProvider key={segment.name}>
<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 !== segment.name &&
"opacity-40",
)}
onPointerEnter={() => handleHover(segment.name)}
onPointerLeave={() => handleHover(null)}
onFocus={() => handleHover(segment.name)}
onBlur={() => handleHover(null)}
>
<span
className="size-2 rounded-full sm:size-2.5"
style={{ backgroundColor: segment.color }}
/>
<span className="text-xs text-muted-foreground">
{segment.name}
</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: segment.color }}
/>
<span className="font-medium">{segment.name}</span>
<span className="text-muted-foreground tabular-nums">
{Math.round(
(segment.value / totalSegmentSales) * 100,
).toLocaleString("fa-IR")}
٪
</span>
</div>
<span className="text-muted-foreground tabular-nums">
{currencyFormatter.format(segment.value)}
</span>
</div>
</ShadTooltipContent>
</ShadTooltip>
</ShadTooltipProvider>
))}
</div>
<div className="grid grid-cols-[1fr_auto_auto] items-center gap-4">
<span className="text-xs text-muted-foreground">دسته</span>
<span className="text-xs text-muted-foreground">سفارش‌ها</span>
<span className="text-xs text-muted-foreground">نسبت به قبل</span>
</div>
<div className="flex flex-col gap-3">
{categoryBreakdown.map((row) => (
<div
key={row.name}
className="grid grid-cols-[1fr_auto_auto] items-center gap-4"
>
<div className="flex min-w-0 items-center gap-2.5">
<div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted">
<span className="text-xs font-medium text-muted-foreground">
{row.name[0]}
</span>
</div>
<span className="truncate text-sm font-medium">{row.name}</span>
</div>
<span className="text-sm tabular-nums">
{numberFormatter.format(row.orderCount)}
</span>
<div
className={cn(
"flex items-center gap-0.5 text-xs",
row.isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{row.isPositive ? (
<ArrowUpRight className="size-3.5" />
) : (
<ArrowDownRight className="size-3.5" />
)}
{row.changePercent.toLocaleString("fa-IR")}٪
</div>
</div>
))}
</div>
</div>
</div>
);
};
function CustomTooltip({
active,
payload,
label,
colors,
}: any & {
colors: { primary: string; secondary: string };
}) {
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.primary }}
/>
<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.secondary }}
/>
<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");
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 totalGrossSales = 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(totalGrossSales)}
</p>
<p className="text-xs text-muted-foreground">
فروش ناخالص ({periodLabels[period]})
</p>
</div>
<div className="hidden items-center gap-3 sm:flex sm:gap-5">
<div className="flex items-center gap-1.5">
<div
className="size-2.5 rounded-full sm:size-3"
style={{ backgroundColor: palette.primary }}
/>
<span className="text-[10px] text-muted-foreground sm:text-xs">
فروش ناخالص
</span>
</div>
<div className="flex items-center gap-1.5">
<div
className="size-2.5 rounded-full sm:size-3"
style={{ backgroundColor: palette.secondary.light }}
/>
<span className="text-[10px] text-muted-foreground sm:text-xs">
سال قبل
</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={revenueChartConfig} className="h-full w-full">
<BarChart data={chartData} barGap={2}>
<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={{
primary: "var(--color-thisYear)",
secondary: "var(--color-prevYear)",
}}
/>
}
cursor={{ fillOpacity: 0.05, radius: 4 }}
/>
<Bar
dataKey="thisYear"
fill="var(--color-thisYear)"
radius={[4, 4, 0, 0]}
maxBarSize={24}
/>
<Bar
dataKey="prevYear"
fill="var(--color-prevYear)"
fillOpacity={0.4}
radius={[4, 4, 0, 0]}
maxBarSize={24}
/>
</BarChart>
</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 RecentOrdersTable = () => {
const [searchQuery, setSearchQuery] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState<string>("all");
const [currentPage, setCurrentPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(10);
const [isHydrated, setIsHydrated] = React.useState(false);
const skipNextReset = React.useRef(true);
const hasActiveFilters = statusFilter !== "all";
const clearFilters = () => {
setStatusFilter("all");
};
const filteredOrders = React.useMemo(() => {
return orders.filter((order) => {
const q = searchQuery.toLowerCase();
const matchesSearch =
order.orderNumber.toLowerCase().includes(q) ||
order.customer.toLowerCase().includes(q) ||
order.category.toLowerCase().includes(q);
const matchesStatus =
statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus;
});
}, [searchQuery, statusFilter]);
const totalPages = Math.ceil(filteredOrders.length / pageSize);
const paginatedOrders = React.useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredOrders.slice(startIndex, endIndex);
}, [filteredOrders, currentPage, pageSize]);
React.useEffect(() => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
const statusParam = params.get("status");
const pageParam = Number.parseInt(params.get("page") ?? "", 10);
const pageSizeParam = Number.parseInt(params.get("pageSize") ?? "", 10);
setSearchQuery(params.get("q") ?? "");
setStatusFilter(
statusParam && orderStatuses.includes(statusParam as OrderStatus)
? statusParam
: "all",
);
setCurrentPage(Number.isFinite(pageParam) && pageParam > 0 ? pageParam : 1);
setPageSize(PAGE_SIZE_OPTIONS.includes(pageSizeParam) ? pageSizeParam : 10);
setIsHydrated(true);
}, []);
React.useEffect(() => {
if (!isHydrated) return;
if (skipNextReset.current) {
skipNextReset.current = false;
return;
}
setCurrentPage(1);
}, [searchQuery, statusFilter, pageSize, isHydrated]);
React.useEffect(() => {
if (!isHydrated) return;
if (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)));
};
return (
<div className="rounded-xl border bg-card">
<div className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:gap-4 sm:px-6 sm:py-3.5">
<div className="flex flex-1 items-center gap-2 sm:gap-2.5">
<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>
<div className="flex flex-wrap items-center gap-2">
<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 sm:size-5"
aria-hidden="true"
/>
<Input
placeholder="جستجوی سفارش، مشتری، دسته…"
value={searchQuery}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchQuery(e.target.value)
}
name="orders-search"
inputMode="search"
autoComplete="off"
aria-label="جستجوی سفارش‌ها"
className="h-8 w-full ps-9 text-sm sm:h-9 sm:w-[160px] sm:ps-10 lg:w-[200px]"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={cn(
"h-8 gap-1.5 sm:h-9 sm:gap-2",
hasActiveFilters && "border-primary",
)}
aria-label="فیلتر سفارش‌ها"
>
<Filter className="size-3.5 sm:size-4" aria-hidden="true" />
<span className="hidden sm:inline">فیلتر</span>
{hasActiveFilters && (
<span className="size-1.5 rounded-full bg-primary sm:size-2" />
)}
</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>
))}
{hasActiveFilters && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearFilters}
className="text-destructive"
>
<X className="me-2 size-4" aria-hidden="true" />
پاک کردن فیلترها
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{hasActiveFilters && (
<div className="flex flex-wrap items-center gap-2 px-3 pb-3 sm:px-6">
<span className="text-[10px] text-muted-foreground sm:text-xs">
فیلترها:
</span>
{statusFilter !== "all" && (
<button
type="button"
className="inline-flex cursor-pointer items-center gap-1 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"
onClick={() => setStatusFilter("all")}
aria-label={`حذف فیلتر ${orderStatusLabels[statusFilter as OrderStatus]}`}
>
{orderStatusLabels[statusFilter as OrderStatus]}
<X className="size-2.5 sm:size-3" />
</button>
)}
</div>
)}
<div className="overflow-x-auto px-3 pb-3 sm:px-6 sm:pb-4">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className="w-[40px] text-xs font-medium text-muted-foreground sm:text-sm">
#
</TableHead>
<TableHead className="min-w-[160px] text-xs font-medium text-muted-foreground sm:text-sm">
سفارش
</TableHead>
<TableHead className="hidden min-w-[128px] text-xs font-medium text-muted-foreground sm:table-cell sm:text-sm">
دسته
</TableHead>
<TableHead className="hidden min-w-[140px] text-xs font-medium text-muted-foreground sm:text-sm md:table-cell">
مشتری
</TableHead>
<TableHead className="min-w-[100px] text-xs font-medium text-muted-foreground sm:text-sm">
وضعیت
</TableHead>
<TableHead className="min-w-[90px] 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="w-[40px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedOrders.length === 0 ? (
<TableRow>
<TableCell
colSpan={8}
className="h-24 text-center text-sm text-muted-foreground"
>
سفارشی با این فیلترها یافت نشد.
</TableCell>
</TableRow>
) : (
paginatedOrders.map((order, index) => (
<TableRow key={order.id}>
<TableCell className="text-xs font-medium sm:text-sm">
{((currentPage - 1) * pageSize + index + 1).toLocaleString(
"fa-IR",
)}
</TableCell>
<TableCell>
<div className="min-w-0">
<span className="block truncate text-xs font-medium sm:text-sm">
{order.orderNumber}
</span>
<span className="text-[10px] text-muted-foreground sm:text-xs">
{order.productCount.toLocaleString("fa-IR")} قلم
</span>
</div>
</TableCell>
<TableCell className="hidden max-w-[160px] truncate text-xs text-muted-foreground sm:table-cell sm:text-sm">
{categoryLabels[order.category] ?? order.category}
</TableCell>
<TableCell className="hidden md:table-cell">
<div className="flex items-center gap-2">
<Avatar className="size-6 bg-muted">
<AvatarFallback className="text-[8px] font-semibold text-muted-foreground uppercase">
{order.customerInitials}
</AvatarFallback>
</Avatar>
<span className="text-xs text-muted-foreground sm:text-sm">
{order.customer}
</span>
</div>
</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>
<TableCell className="text-xs text-foreground tabular-nums sm:text-sm">
{currencyFormatter.format(order.total)}
</TableCell>
<TableCell className="hidden text-xs text-muted-foreground sm:table-cell sm:text-sm">
{formatOrderDate(order.date)}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground hover:text-foreground sm:size-8"
aria-label={`عملیات ${order.orderNumber}`}
>
<MoreHorizontal className="size-3.5 sm:size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Eye className="me-2 size-4" aria-hidden="true" />
مشاهده جزئیات
</DropdownMenuItem>
<DropdownMenuItem>
<Pencil className="me-2 size-4" aria-hidden="true" />
ویرایش سفارش
</DropdownMenuItem>
<DropdownMenuItem>
<Copy className="me-2 size-4" aria-hidden="true" />
تکثیر
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onSelect={(event: Event) => {
event.preventDefault();
if (typeof window === "undefined") return;
window.confirm(
"این سفارش لغو شود؟ این عمل قابل بازگشت نیست.",
);
}}
>
<X className="me-2 size-4" aria-hidden="true" />
لغو سفارش
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col items-center justify-between gap-3 border-t px-3 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: string) => 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 className="text-muted-foreground">
{filteredOrders.length === 0
? "۰"
: `${((currentPage - 1) * pageSize + 1).toLocaleString("fa-IR")}-${Math.min(
currentPage * pageSize,
filteredOrders.length,
).toLocaleString("fa-IR")}`}{" "}
از {filteredOrders.length.toLocaleString("fa-IR")}
</span>
</div>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => goToPage(1)}
disabled={currentPage === 1}
aria-label="رفتن به صفحه اول"
>
<ChevronsLeft className="size-4 rtl:rotate-180" />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
aria-label="رفتن به صفحه قبل"
>
<ChevronLeft className="size-4 rtl:rotate-180" />
</Button>
<div className="flex items-center gap-1 px-2">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let pageNum: number;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "ghost"}
size="icon"
className="size-8"
onClick={() => goToPage(pageNum)}
>
{pageNum.toLocaleString("fa-IR")}
</Button>
);
})}
</div>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="رفتن به صفحه بعد"
>
<ChevronRight className="size-4 rtl:rotate-180" />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => goToPage(totalPages)}
disabled={currentPage === totalPages}
aria-label="رفتن به صفحه آخر"
>
<ChevronsRight className="size-4 rtl:rotate-180" />
</Button>
</div>
</div>
</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"
>
<StatsCards />
<div className="flex flex-col gap-4 sm:gap-6 xl:flex-row">
<RevenueFlowChart />
<SegmentationPanel />
</div>
<RecentOrdersTable />
</div>
);
};
const Dashboard4 = ({ 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 { Dashboard4 };