Files
reui-blocks/blocks/dashboard4.tsx
T

1851 lines
60 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 {
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 };