Files
reui-blocks/blocks/dashboard9.tsx
T

1715 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
BarChart3,
Bell,
Box,
ChevronLeft,
ChevronRight,
ChevronsUpDown,
ClipboardList,
Download,
Globe,
HelpCircle,
LayoutDashboard,
LogOut,
MessageSquare,
Minus,
MoreHorizontal,
Package,
RotateCcw,
Settings,
TrendingDown,
TrendingUp,
Truck,
User,
Users,
Wallet,
} from "lucide-react";
import * as React from "react";
import type { TooltipProps } from "recharts";
import {
Bar,
BarChart,
CartesianGrid,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
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 { TooltipProvider } 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 RichMetricStatItem = {
title: string;
value: number;
trendValue: number;
footerDelta: number;
footerSubtextCount: number;
};
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
type Order = {
id: string;
orderNumber: string;
customer: string;
total: number;
status: OrderStatus;
};
type FulfillmentItem = {
order: string;
shipped: Date;
progress: number;
segments: number[];
};
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
});
const numberFormatter = new Intl.NumberFormat("fa-IR");
const trendPercentFormatter = new Intl.NumberFormat("fa-IR", {
style: "percent",
maximumFractionDigits: 1,
signDisplay: "exceptZero",
});
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
notation: "compact",
maximumFractionDigits: 0,
});
const monthFormatter = new Intl.DateTimeFormat("fa-IR", { month: "short" });
const dashboardRangeFormatter = new Intl.DateTimeFormat("fa-IR", {
month: "short",
day: "numeric",
year: "numeric",
});
const dashboardRangeStart = new Date(2025, 0, 1);
const dashboardRangeEnd = new Date(2025, 0, 31);
const dashboardDateRangeLabel =
typeof dashboardRangeFormatter.formatRange === "function"
? dashboardRangeFormatter.formatRange(
dashboardRangeStart,
dashboardRangeEnd,
)
: `${dashboardRangeFormatter.format(
dashboardRangeStart,
)} – ${dashboardRangeFormatter.format(dashboardRangeEnd)}`;
const shippedDateFormatter = new Intl.DateTimeFormat("fa-IR", {
month: "short",
day: "numeric",
year: "numeric",
});
const orderStatusLabels: Record<OrderStatus, string> = {
Processing: "در حال پردازش",
Shipped: "ارسال‌شده",
Delivered: "تحویل‌شده",
Cancelled: "لغوشده",
};
const formatTrendValue = (trendValue: number) => {
return trendPercentFormatter.format(trendValue / 100);
};
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: Wallet, href: "#" },
],
},
],
footerGroup: {
title: "تنظیمات",
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
},
user: {
name: "علی رضایی",
email: "john@acme.store",
avatar:
"/block-media/shadcnblocks/block/avatar-8.webp",
},
};
const richMetricStats: RichMetricStatItem[] = [
{
title: "فروش حضوری",
value: 7820.75,
trendValue: 4.3,
footerDelta: 322.5,
footerSubtextCount: 5000,
},
{
title: "فروش آنلاین",
value: 985_937.45,
trendValue: 12.5,
footerDelta: 109_500,
footerSubtextCount: 21_000,
},
{
title: "عمده‌فروشی",
value: 124_650,
trendValue: 6.8,
footerDelta: 7_935,
footerSubtextCount: 890,
},
];
const monthLabel = (monthIndex: number) =>
monthFormatter.format(new Date(2025, monthIndex, 1));
const salesPipelineData: Record<
string,
{ week: string; month: string; orders: number; sales: number }[]
> = {
q1: [
{ week: "W1", month: monthLabel(0), orders: 220, sales: 5100 },
{ week: "W2", month: monthLabel(0), orders: 480, sales: 11200 },
{ week: "W3", month: monthLabel(0), orders: 390, sales: 9400 },
{ week: "W4", month: monthLabel(0), orders: 150, sales: 3600 },
{ week: "W5", month: monthLabel(1), orders: 310, sales: 7400 },
{ week: "W6", month: monthLabel(1), orders: 540, sales: 13100 },
{ week: "W7", month: monthLabel(1), orders: 460, sales: 10800 },
{ week: "W8", month: monthLabel(1), orders: 200, sales: 4700 },
{ week: "W9", month: monthLabel(2), orders: 130, sales: 3100 },
{ week: "W10", month: monthLabel(2), orders: 420, sales: 10200 },
{ week: "W11", month: monthLabel(2), orders: 510, sales: 12400 },
{ week: "W12", month: monthLabel(2), orders: 350, sales: 8500 },
],
q2: [
{ week: "W1", month: monthLabel(3), orders: 410, sales: 9800 },
{ week: "W2", month: monthLabel(3), orders: 280, sales: 6700 },
{ week: "W3", month: monthLabel(3), orders: 120, sales: 2900 },
{ week: "W4", month: monthLabel(3), orders: 350, sales: 8400 },
{ week: "W5", month: monthLabel(4), orders: 520, sales: 12600 },
{ week: "W6", month: monthLabel(4), orders: 470, sales: 11300 },
{ week: "W7", month: monthLabel(4), orders: 190, sales: 4500 },
{ week: "W8", month: monthLabel(4), orders: 100, sales: 2400 },
{ week: "W9", month: monthLabel(5), orders: 330, sales: 7900 },
{ week: "W10", month: monthLabel(5), orders: 490, sales: 11800 },
{ week: "W11", month: monthLabel(5), orders: 540, sales: 13000 },
{ week: "W12", month: monthLabel(5), orders: 260, sales: 6200 },
],
q3: [
{ week: "W1", month: monthLabel(6), orders: 180, sales: 4200 },
{ week: "W2", month: monthLabel(6), orders: 520, sales: 12800 },
{ week: "W3", month: monthLabel(6), orders: 480, sales: 11500 },
{ week: "W4", month: monthLabel(6), orders: 120, sales: 2800 },
{ week: "W5", month: monthLabel(7), orders: 90, sales: 2100 },
{ week: "W6", month: monthLabel(7), orders: 450, sales: 10500 },
{ week: "W7", month: monthLabel(7), orders: 510, sales: 12200 },
{ week: "W8", month: monthLabel(7), orders: 480, sales: 11000 },
{ week: "W9", month: monthLabel(8), orders: 200, sales: 4800 },
{ week: "W10", month: monthLabel(8), orders: 150, sales: 3500 },
{ week: "W11", month: monthLabel(8), orders: 380, sales: 9200 },
{ week: "W12", month: monthLabel(8), orders: 420, sales: 10100 },
],
q4: [
{ week: "W1", month: monthLabel(9), orders: 300, sales: 7200 },
{ week: "W2", month: monthLabel(9), orders: 160, sales: 3800 },
{ week: "W3", month: monthLabel(9), orders: 440, sales: 10600 },
{ week: "W4", month: monthLabel(9), orders: 530, sales: 12900 },
{ week: "W5", month: monthLabel(10), orders: 380, sales: 9100 },
{ week: "W6", month: monthLabel(10), orders: 140, sales: 3400 },
{ week: "W7", month: monthLabel(10), orders: 250, sales: 6000 },
{ week: "W8", month: monthLabel(10), orders: 500, sales: 12100 },
{ week: "W9", month: monthLabel(11), orders: 550, sales: 13300 },
{ week: "W10", month: monthLabel(11), orders: 470, sales: 11400 },
{ week: "W11", month: monthLabel(11), orders: 210, sales: 5000 },
{ week: "W12", month: monthLabel(11), orders: 340, sales: 8200 },
],
};
const fullYearData = [
{ monthIndex: 0, thisYear: 42000, prevYear: 38000 },
{ monthIndex: 1, thisYear: 38000, prevYear: 45000 },
{ monthIndex: 2, thisYear: 52000, prevYear: 41000 },
{ monthIndex: 3, thisYear: 45000, prevYear: 48000 },
{ monthIndex: 4, thisYear: 58000, prevYear: 44000 },
{ monthIndex: 5, thisYear: 41000, prevYear: 52000 },
{ monthIndex: 6, thisYear: 55000, prevYear: 47000 },
{ monthIndex: 7, thisYear: 48000, prevYear: 53000 },
{ monthIndex: 8, thisYear: 62000, prevYear: 49000 },
{ monthIndex: 9, thisYear: 54000, prevYear: 58000 },
{ monthIndex: 10, thisYear: 67000, prevYear: 52000 },
{ monthIndex: 11, thisYear: 71000, prevYear: 61000 },
].map(({ monthIndex, ...entry }) => ({
month: monthLabel(monthIndex),
...entry,
}));
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 statusStyles: Record<OrderStatus, string> = {
Processing:
"bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-400/20",
Shipped:
"bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-400/20",
Delivered:
"bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-900/30 dark:text-emerald-400 dark:ring-emerald-400/20",
Cancelled:
"bg-red-50 text-red-700 ring-1 ring-inset ring-red-600/10 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-400/20",
};
const orders: Order[] = [
{
id: "1",
orderNumber: "ORD-2024-001",
customer: "Sarah Johnson",
status: "Delivered",
total: 2499.0,
},
{
id: "2",
orderNumber: "ORD-2024-002",
customer: "Michael Chen",
status: "Shipped",
total: 1348.0,
},
{
id: "3",
orderNumber: "ORD-2024-003",
customer: "Emma Wilson",
status: "Processing",
total: 1198.0,
},
{
id: "4",
orderNumber: "ORD-2024-004",
customer: "James Rodriguez",
status: "Delivered",
total: 799.0,
},
{
id: "5",
orderNumber: "ORD-2024-005",
customer: "Lisa Park",
status: "Cancelled",
total: 599.0,
},
{
id: "6",
orderNumber: "ORD-2024-006",
customer: "David Kim",
status: "Shipped",
total: 5498.0,
},
{
id: "7",
orderNumber: "ORD-2024-007",
customer: "Anna Martinez",
status: "Delivered",
total: 1199.0,
},
{
id: "8",
orderNumber: "ORD-2024-008",
customer: "Robert Taylor",
status: "Processing",
total: 1128.0,
},
{
id: "9",
orderNumber: "ORD-2024-009",
customer: "Jennifer Lee",
status: "Shipped",
total: 449.0,
},
{
id: "10",
orderNumber: "ORD-2024-010",
customer: "William Brown",
status: "Delivered",
total: 2199.0,
},
{
id: "11",
orderNumber: "ORD-2024-011",
customer: "Sophia Davis",
status: "Cancelled",
total: 349.0,
},
{
id: "12",
orderNumber: "ORD-2024-012",
customer: "Daniel Garcia",
status: "Processing",
total: 899.0,
},
];
const fulfillmentData: FulfillmentItem[] = [
{
order: "ORD-4821",
shipped: new Date(2025, 0, 27),
progress: 92,
segments: [
0.9, 0.7, 1.0, 0.8, 0.6, 0.9, 1.0, 0.7, 0.5, 0.8, 0.9, 1.0, 0.6, 0.7, 0.8,
0.9, 1.0, 0.5, 0.7, 0.8, 0.9, 0.6, 1.0, 0.8, 0.7, 0.3, 0.2, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4819",
shipped: new Date(2025, 0, 26),
progress: 78,
segments: [
0.8, 0.6, 0.9, 0.7, 1.0, 0.5, 0.8, 0.9, 0.6, 0.7, 1.0, 0.8, 0.5, 0.9, 0.7,
0.6, 0.8, 1.0, 0.7, 0.5, 0.2, 0.1, 0.15, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1,
],
},
{
order: "ORD-4815",
shipped: new Date(2025, 0, 25),
progress: 100,
segments: [
1.0, 0.9, 0.8, 1.0, 0.7, 0.9, 1.0, 0.8, 0.6, 0.9, 1.0, 0.7, 0.8, 0.9, 1.0,
0.6, 0.8, 0.9, 1.0, 0.7, 0.9, 0.8, 1.0, 0.6, 0.9, 0.7, 1.0, 0.8, 0.9, 1.0,
],
},
{
order: "ORD-4812",
shipped: new Date(2025, 0, 24),
progress: 65,
segments: [
0.9, 1.0, 0.7, 0.8, 0.6, 0.9, 0.5, 0.8, 1.0, 0.7, 0.9, 0.6, 0.8, 0.5, 0.7,
1.0, 0.6, 0.9, 0.8, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4808",
shipped: new Date(2025, 0, 23),
progress: 43,
segments: [
0.8, 0.7, 1.0, 0.6, 0.9, 0.8, 0.5, 0.7, 1.0, 0.9, 0.6, 0.8, 0.7, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4805",
shipped: new Date(2025, 0, 22),
progress: 100,
segments: [
0.9, 0.8, 1.0, 0.7, 0.9, 0.6, 1.0, 0.8, 0.7, 0.9, 1.0, 0.8, 0.6, 0.9, 0.7,
1.0, 0.8, 0.9, 0.7, 1.0, 0.8, 0.9, 0.6, 1.0, 0.7, 0.8, 0.9, 1.0, 0.8, 0.9,
],
},
{
order: "ORD-4801",
shipped: new Date(2025, 0, 21),
progress: 88,
segments: [
1.0, 0.8, 0.7, 0.9, 0.6, 1.0, 0.8, 0.5, 0.9, 0.7, 1.0, 0.8, 0.6, 0.9, 0.7,
0.8, 1.0, 0.6, 0.5, 0.3, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4798",
shipped: new Date(2025, 0, 20),
progress: 55,
segments: [
0.7, 0.9, 1.0, 0.6, 0.8, 0.9, 0.7, 1.0, 0.5, 0.8, 0.6, 0.9, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4794",
shipped: new Date(2025, 0, 19),
progress: 100,
segments: [
0.8, 1.0, 0.9, 0.7, 0.8, 1.0, 0.6, 0.9, 0.8, 1.0, 0.7, 0.9, 0.8, 1.0, 0.6,
0.9, 0.7, 1.0, 0.8, 0.9, 0.7, 1.0, 0.8, 0.6, 0.9, 1.0, 0.7, 0.8, 0.9, 1.0,
],
},
{
order: "ORD-4790",
shipped: new Date(2025, 0, 18),
progress: 71,
segments: [
0.9, 0.6, 0.8, 1.0, 0.7, 0.9, 0.5, 0.8, 1.0, 0.6, 0.9, 0.7, 0.8, 0.5, 0.3,
0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4786",
shipped: new Date(2025, 0, 17),
progress: 35,
segments: [
1.0, 0.8, 0.9, 0.7, 0.6, 0.8, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
],
},
{
order: "ORD-4782",
shipped: new Date(2025, 0, 16),
progress: 96,
segments: [
0.8, 0.9, 1.0, 0.7, 0.8, 0.9, 1.0, 0.6, 0.8, 0.9, 1.0, 0.7, 0.9, 0.8, 1.0,
0.6, 0.9, 0.7, 1.0, 0.8, 0.9, 0.6, 1.0, 0.8, 0.7, 0.9, 1.0, 0.8, 0.3, 0.1,
],
},
];
const ordersBarConfig = {
orders: { label: "سفارش‌ها", color: palette.primary },
} satisfies ChartConfig;
const salesBarConfig = {
sales: { label: "فروش", theme: palette.secondary },
} satisfies ChartConfig;
const createHighlightBarShape = (fill: string) => (props: unknown) => {
const { x, y, width, height, index } = props as {
x: number;
y: number;
width: number;
height: number;
index: number;
};
const isHighlight = index === 5;
return (
<rect
x={x}
y={y}
width={width}
height={height}
fill={fill}
opacity={isHighlight ? 1 : 0.45}
rx={4}
ry={4}
/>
);
};
const revenueFlowChartConfig = {
thisYear: { label: "امسال", color: palette.primary },
prevYear: { label: "سال قبل", theme: palette.secondary },
} satisfies ChartConfig;
const tableHeadClass = "text-xs font-medium text-muted-foreground sm:text-sm";
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" 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">
<div className="flex flex-col gap-0.5">
<h1 className="text-base font-medium text-pretty">
نمای کلی داشبورد
</h1>
<p className="hidden text-xs text-muted-foreground sm:block">
خلاصه‌ای از خریدها، فروش‌ها و سایر شاخص‌ها
</p>
</div>
<div className="ms-auto flex items-center gap-2">
<span className="hidden text-xs text-muted-foreground lg:inline">
{dashboardDateRangeLabel}
</span>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs"
aria-label="همه پلتفرم‌ها"
>
<Globe className="size-3.5" aria-hidden="true" />
<span className="hidden sm:inline">همه پلتفرم‌ها</span>
</Button>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs"
aria-label="همه محصولات"
>
<span className="hidden sm:inline">همه محصولات</span>
</Button>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs"
aria-label="خروجی"
>
<Download className="size-3.5" aria-hidden="true" />
<span className="hidden sm:inline">خروجی</span>
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
aria-label="اعلان‌ها"
>
<Bell className="size-4" aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
aria-label="راهنما"
>
<HelpCircle className="size-4" aria-hidden="true" />
</Button>
</div>
</header>
);
};
const AccountingStatsCards = () => {
return (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{richMetricStats.map((stat) => {
const isPositive = stat.trendValue > 0;
const isNeutral = stat.trendValue === 0;
const Icon = isNeutral ? Minus : isPositive ? TrendingUp : TrendingDown;
const trendClass = isNeutral
? "text-foreground bg-muted"
: isPositive
? "text-success border-success/20 bg-success/10"
: "text-destructive border-destructive/20 bg-destructive/10";
const formattedTrend = formatTrendValue(stat.trendValue);
const deltaPrefix =
stat.footerDelta > 0 ? "+" : stat.footerDelta < 0 ? "-" : "";
const footerLabel = `${deltaPrefix}${currencyFormatter.format(
Math.abs(stat.footerDelta),
)} نسبت به ماه قبل`;
const footerSubtext = `بر اساس ${numberFormatter.format(
stat.footerSubtextCount,
)} سفارش`;
return (
<Card key={stat.title} className="@container/card shadow-none">
<CardHeader className="flex items-start justify-between gap-2">
<div>
<CardDescription className="font-medium">
{stat.title}
</CardDescription>
<CardTitle className="text-xl font-bold @[600px]/card:text-xl @[800px]/card:text-2xll">
{currencyFormatter.format(stat.value)}
</CardTitle>
</div>
<Badge
variant="outline"
className={cn("gap-1 px-1.5 py-0.5", trendClass)}
>
<Icon className="size-3" aria-hidden="true" />
{formattedTrend}
</Badge>
</CardHeader>
<CardFooter className="flex-col items-start gap-1 text-sm">
<div className="line-clamp-1 flex items-center gap-1.5 font-medium">
<span
className={
isNeutral
? ""
: isPositive
? "text-success"
: "text-destructive"
}
>
{footerLabel}
</span>
<Icon
className={cn(
"size-3.5",
isNeutral
? "text-muted-foreground"
: isPositive
? "text-success"
: "text-destructive",
)}
aria-hidden="true"
/>
</div>
<div className="text-xs text-muted-foreground">
{footerSubtext}
</div>
</CardFooter>
</Card>
);
})}
</div>
);
};
function PipelineTooltip({
active,
payload,
label,
valueFormatter,
}: any & {
valueFormatter: (v: number) => string;
}) {
if (!active || !payload?.length) return null;
const entry = payload[0];
return (
<div className="rounded-lg border border-border bg-popover px-3 py-2 shadow-lg">
<p className="mb-1 text-xs font-medium text-foreground">{label}</p>
<p className="text-sm font-semibold text-foreground">
{valueFormatter(Number(entry.value))}
</p>
</div>
);
}
const SalesPipelineChart = () => {
const [searchParams, setSearchParams] = React.useState(
() =>
new URLSearchParams(
typeof window !== "undefined" ? window.location.search : "",
),
);
const quarter = searchParams.get("quarter") ?? "q1";
const handleQuarterChange = (value: string) => {
const next = new URLSearchParams(searchParams);
next.set("quarter", value);
setSearchParams(next);
window.history.replaceState(null, "", `?${next.toString()}`);
};
const data = salesPipelineData[quarter] ?? salesPipelineData.q1;
const totalOrders = data.reduce((sum, d) => sum + d.orders, 0);
const totalSales = data.reduce((sum, d) => sum + d.sales, 0);
return (
<div className="flex min-w-0 flex-1 flex-col rounded-xl border bg-card">
<div className="flex h-14 items-center justify-between border-b 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="قیف فروش"
>
<BarChart3
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>
<Select value={quarter} onValueChange={handleQuarterChange}>
<SelectTrigger
className="h-7 w-[120px] text-xs"
aria-label="انتخاب سه‌ماهه"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="q1">سه‌ماهه ۱</SelectItem>
<SelectItem value="q2">سه‌ماهه ۲</SelectItem>
<SelectItem value="q3">سه‌ماهه ۳</SelectItem>
<SelectItem value="q4">سه‌ماهه ۴</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid flex-1 grid-cols-1 gap-4 p-4 sm:grid-cols-[1fr_auto_1fr] sm:p-5">
<div className="flex min-h-0 min-w-0 flex-col gap-3 overflow-hidden">
<div>
<p className="text-lg font-semibold tracking-tight">
{numberFormatter.format(totalOrders)}
</p>
<p className="text-[10px] tracking-wider text-muted-foreground uppercase">
مجموع سفارش‌ها
</p>
</div>
<div className="min-h-0 w-full min-w-0 flex-1">
<ChartContainer config={ordersBarConfig} className="h-full w-full">
<BarChart data={data}>
<CartesianGrid strokeDasharray="0" vertical={false} />
<XAxis
dataKey="week"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dy={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dx={-5}
width={40}
/>
<Tooltip
cursor={{ fillOpacity: 0.05 }}
content={
<PipelineTooltip
valueFormatter={(v: number) => numberFormatter.format(v)}
/>
}
/>
<Bar
dataKey="orders"
radius={[4, 4, 0, 0]}
fill="var(--color-orders)"
shape={createHighlightBarShape("var(--color-orders)")}
/>
</BarChart>
</ChartContainer>
</div>
</div>
<div className="hidden w-px self-stretch bg-border sm:block" />
<div className="flex min-h-0 min-w-0 flex-col gap-3 overflow-hidden">
<div>
<p className="text-lg font-semibold tracking-tight">
{compactCurrencyFormatter.format(totalSales)}
</p>
<p className="text-[10px] tracking-wider text-muted-foreground uppercase">
مجموع فروش
</p>
</div>
<div className="min-h-0 w-full min-w-0 flex-1">
<ChartContainer config={salesBarConfig} className="h-full w-full">
<BarChart data={data}>
<CartesianGrid strokeDasharray="0" vertical={false} />
<XAxis
dataKey="week"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dy={8}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dx={-5}
width={40}
/>
<Tooltip
cursor={{ fillOpacity: 0.05 }}
content={
<PipelineTooltip
valueFormatter={(v: number) => currencyFormatter.format(v)}
/>
}
/>
<Bar
dataKey="sales"
radius={[4, 4, 0, 0]}
fill="var(--color-sales)"
shape={createHighlightBarShape("var(--color-sales)")}
/>
</BarChart>
</ChartContainer>
</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}
</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>("year");
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 !== "year") {
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">
<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" aria-hidden="true" />
</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"
>
<LineChart data={chartData}>
<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={{ strokeOpacity: 0.2 }}
/>
<Line
type="linear"
dataKey="thisYear"
stroke="var(--color-thisYear)"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
dot={{ fill: "var(--color-thisYear)", strokeWidth: 0, r: 2 }}
activeDot={{ r: 3.5, fill: "var(--color-thisYear)" }}
/>
<Line
type="linear"
dataKey="prevYear"
stroke="var(--color-prevYear)"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity={0.5}
dot={{
fill: "var(--color-prevYear)",
fillOpacity: 0.5,
strokeWidth: 0,
r: 2,
}}
activeDot={{
r: 3.5,
fill: "var(--color-prevYear)",
fillOpacity: 0.5,
}}
/>
</LineChart>
</ChartContainer>
</div>
</div>
);
};
const RecentOrdersTable = () => {
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="rounded-xl border bg-card">
<div className="flex items-center justify-between gap-3 px-4 pt-4 sm:px-6">
<div className="flex items-center gap-2">
<h2 className="text-sm font-medium text-pretty sm:text-base">
سفارش‌های اخیر
</h2>
<span className="ms-1 inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-[10px] font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset sm:text-xs dark:bg-gray-800/50 dark:text-gray-400 dark:ring-gray-400/20">
{filteredOrders.length.toLocaleString("fa-IR")}
</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 sm:h-9 sm:gap-2"
aria-label="فیلتر وضعیت سفارش"
>
<span className="text-xs sm:text-sm">
{statusFilter === "all"
? "همه"
: orderStatusLabels[statusFilter]}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[180px]">
<DropdownMenuLabel>فیلتر بر اساس وضعیت</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={statusFilter === "all"}
onCheckedChange={() => setStatusFilter("all")}
>
همه وضعیت‌ها
</DropdownMenuCheckboxItem>
{orderStatuses.map((status) => (
<DropdownMenuCheckboxItem
key={status}
checked={statusFilter === status}
onCheckedChange={() => setStatusFilter(status)}
>
{orderStatusLabels[status]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="px-4 pt-3 pb-4 sm:px-6">
<Table>
<TableHeader>
<TableRow className="bg-muted/50 hover:bg-muted/50">
<TableHead className={tableHeadClass}>مرجع سفارش</TableHead>
<TableHead className={tableHeadClass}>خریدار</TableHead>
<TableHead className={tableHeadClass}>مجموع</TableHead>
<TableHead className={tableHeadClass}>وضعیت</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" aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon"
className="size-7"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="رفتن به صفحه بعد"
>
<ChevronRight className="size-3.5 rtl:rotate-180" aria-hidden="true" />
</Button>
</div>
</div>
</div>
);
};
const FulfillmentPanel = () => {
return (
<div className="flex flex-col gap-4 rounded-xl border bg-card p-5">
<div className="flex items-center justify-between gap-2">
<h2 className="text-sm font-medium text-pretty">تکمیل سفارش</h2>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-7"
aria-label="بروزرسانی"
>
<RotateCcw className="size-3.5" aria-hidden="true" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7"
aria-label="گزینه‌ها"
>
<MoreHorizontal className="size-3.5" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>خروجی جدول</DropdownMenuItem>
<DropdownMenuItem>مشاهده همه سفارش‌ها</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div>
<div className="flex items-center border-b pe-3 pb-2 text-[10px] text-muted-foreground">
<span className="w-20 shrink-0">سفارش</span>
<span className="flex-1">وضعیت</span>
<span className="w-8 shrink-0 text-end">تحویل٪</span>
</div>
<ScrollArea className="h-[280px]">
<div className="divide-y pe-3">
{fulfillmentData.map((row) => (
<div
key={row.order}
className="flex items-center gap-2 py-2.5 text-xs"
>
<span className="w-20 shrink-0 font-medium">{row.order}</span>
<div className="flex min-w-0 flex-1 items-center gap-px overflow-hidden">
{row.segments.slice(0, 15).map((opacity, i) => {
const filled = i < Math.round((row.progress / 100) * 15);
return (
<div
key={i}
className="h-2.5 w-2 shrink-0 rounded-[1px]"
style={{
backgroundColor: filled
? palette.primary
: "var(--muted)",
opacity: filled ? opacity : 0.2,
}}
/>
);
})}
</div>
<span className="w-8 shrink-0 text-end font-medium">
{row.progress.toLocaleString("fa-IR")}
</span>
</div>
))}
</div>
</ScrollArea>
</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"
>
<AccountingStatsCards />
<div className="grid gap-4 sm:gap-6 lg:grid-cols-[3fr_2fr]">
<SalesPipelineChart />
<RevenueFlowChart />
</div>
<div className="grid gap-4 sm:gap-6 lg:grid-cols-[2fr_1fr]">
<RecentOrdersTable />
<FulfillmentPanel />
</div>
</div>
);
};
const Dashboard9 = ({ className }: { className?: string }) => {
return (
<TooltipProvider>
<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>
</TooltipProvider>
);
};
export { Dashboard9 };