2026-07-11 21:21:29 +03:30
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
BarChart3,
|
|
|
|
|
|
Bell,
|
|
|
|
|
|
Box,
|
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
ChevronsLeft,
|
|
|
|
|
|
ChevronsRight,
|
|
|
|
|
|
ChevronsUpDown,
|
|
|
|
|
|
ClipboardList,
|
|
|
|
|
|
Copy,
|
|
|
|
|
|
Eye,
|
|
|
|
|
|
Filter,
|
|
|
|
|
|
Globe,
|
|
|
|
|
|
HelpCircle,
|
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
|
LogOut,
|
|
|
|
|
|
MessageSquare,
|
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
|
Package,
|
|
|
|
|
|
Pencil,
|
|
|
|
|
|
RotateCcw,
|
|
|
|
|
|
Search,
|
|
|
|
|
|
Settings,
|
|
|
|
|
|
Truck,
|
|
|
|
|
|
User,
|
|
|
|
|
|
Users,
|
|
|
|
|
|
X,
|
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
import * as React from "react";
|
|
|
|
|
|
import {
|
|
|
|
|
|
CartesianGrid,
|
|
|
|
|
|
Line,
|
|
|
|
|
|
LineChart,
|
|
|
|
|
|
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 { 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 StatGridItem = {
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
value: number;
|
|
|
|
|
|
format: "currency" | "number" | "percent" | "days" | "compact";
|
|
|
|
|
|
change: number;
|
|
|
|
|
|
invertColor: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type FulfillmentItem = {
|
|
|
|
|
|
order: string;
|
|
|
|
|
|
shipped: Date;
|
|
|
|
|
|
progress: number;
|
|
|
|
|
|
segments: number[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
|
|
|
|
|
|
|
|
|
|
|
|
type Order = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
orderNumber: string;
|
|
|
|
|
|
customer: string;
|
|
|
|
|
|
customerInitials: string;
|
|
|
|
|
|
products: string[];
|
|
|
|
|
|
productCount: number;
|
|
|
|
|
|
status: OrderStatus;
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
date: Date;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
style: "currency",
|
|
|
|
|
|
currency: "IRR",
|
|
|
|
|
|
});
|
|
|
|
|
|
const numberFormatter = new Intl.NumberFormat("fa-IR");
|
|
|
|
|
|
const compactNumberFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
notation: "compact",
|
|
|
|
|
|
});
|
|
|
|
|
|
const percentFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
style: "percent",
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
const oneDecimalFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
const weekdayFormatter = new Intl.DateTimeFormat("fa-IR", {
|
|
|
|
|
|
weekday: "short",
|
|
|
|
|
|
});
|
|
|
|
|
|
const monthDayFormatter = new Intl.DateTimeFormat("fa-IR", {
|
|
|
|
|
|
month: "short",
|
|
|
|
|
|
day: "numeric",
|
|
|
|
|
|
});
|
|
|
|
|
|
const dateFormatter = new Intl.DateTimeFormat("fa-IR", {
|
|
|
|
|
|
month: "short",
|
|
|
|
|
|
day: "numeric",
|
|
|
|
|
|
year: "numeric",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const orderStatusLabels: Record<OrderStatus, string> = {
|
|
|
|
|
|
Processing: "در حال پردازش",
|
|
|
|
|
|
Shipped: "ارسالشده",
|
|
|
|
|
|
Delivered: "تحویلشده",
|
|
|
|
|
|
Cancelled: "لغوشده",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatStatValue = (stat: StatGridItem) => {
|
|
|
|
|
|
switch (stat.format) {
|
|
|
|
|
|
case "currency":
|
|
|
|
|
|
return currencyFormatter.format(stat.value);
|
|
|
|
|
|
case "percent":
|
|
|
|
|
|
return percentFormatter.format(stat.value);
|
|
|
|
|
|
case "days":
|
|
|
|
|
|
return `${oneDecimalFormatter.format(stat.value)} روز`;
|
|
|
|
|
|
case "compact":
|
|
|
|
|
|
return compactNumberFormatter.format(stat.value);
|
|
|
|
|
|
default:
|
|
|
|
|
|
return numberFormatter.format(stat.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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: {
|
2026-07-15 20:22:14 +02:00
|
|
|
|
src: "/block-media/shadcnblocks/block/logos/NovaSales-logo.svg",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
alt: "فروشگاه آکمه",
|
|
|
|
|
|
title: "ری یو آی Store",
|
|
|
|
|
|
description: "فروشگاه آنلاین",
|
|
|
|
|
|
},
|
|
|
|
|
|
navGroups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "اصلی",
|
|
|
|
|
|
defaultOpen: true,
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "داشبورد",
|
|
|
|
|
|
icon: LayoutDashboard,
|
|
|
|
|
|
href: "#",
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ label: "سفارشها", icon: ClipboardList, href: "#" },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "محصولات",
|
|
|
|
|
|
icon: Box,
|
|
|
|
|
|
href: "#",
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ label: "همه محصولات", icon: Package, href: "#" },
|
|
|
|
|
|
{ label: "دستهبندیها", icon: Package, href: "#" },
|
|
|
|
|
|
{ label: "موجودی", icon: Package, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{ label: "مشتریان", icon: Users, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-07-15 20:22:14 +02:00
|
|
|
|
title: "تحلیل فروش",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
defaultOpen: true,
|
|
|
|
|
|
items: [
|
2026-07-15 20:22:14 +02:00
|
|
|
|
{ label: "تحلیل فروش", icon: BarChart3, href: "#" },
|
2026-07-11 21:21:29 +03:30
|
|
|
|
{ label: "گزارشها", icon: Globe, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "سایر",
|
|
|
|
|
|
defaultOpen: false,
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{ label: "ارسال", icon: Truck, href: "#" },
|
|
|
|
|
|
{ label: "پیامها", icon: MessageSquare, href: "#" },
|
|
|
|
|
|
{ label: "مرجوعیها", icon: RotateCcw, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
footerGroup: {
|
|
|
|
|
|
title: "تنظیمات",
|
|
|
|
|
|
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
|
|
|
|
|
|
},
|
|
|
|
|
|
user: {
|
|
|
|
|
|
name: "علی رضایی",
|
|
|
|
|
|
email: "john@acme.store",
|
|
|
|
|
|
avatar:
|
|
|
|
|
|
"/block-media/shadcnblocks/block/avatar-5.webp",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const statsGridData: StatGridItem[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "درآمد کل",
|
|
|
|
|
|
value: 1_800_000,
|
|
|
|
|
|
format: "currency",
|
|
|
|
|
|
change: 12.0,
|
|
|
|
|
|
invertColor: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "میانگین تکمیل",
|
|
|
|
|
|
value: 2.4,
|
|
|
|
|
|
format: "days",
|
|
|
|
|
|
change: -5.0,
|
|
|
|
|
|
invertColor: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "سفارشها",
|
|
|
|
|
|
value: 9420,
|
|
|
|
|
|
format: "number",
|
|
|
|
|
|
change: 18.0,
|
|
|
|
|
|
invertColor: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "نرخ تبدیل",
|
|
|
|
|
|
value: 0.042,
|
|
|
|
|
|
format: "percent",
|
|
|
|
|
|
change: 1.2,
|
|
|
|
|
|
invertColor: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "نرخ مرجوعی",
|
|
|
|
|
|
value: 0.031,
|
|
|
|
|
|
format: "percent",
|
|
|
|
|
|
change: -11.0,
|
|
|
|
|
|
invertColor: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "بازدید صفحات",
|
|
|
|
|
|
value: 184_000,
|
|
|
|
|
|
format: "compact",
|
|
|
|
|
|
change: 22.0,
|
|
|
|
|
|
invertColor: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const categoryWeekData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 6)),
|
|
|
|
|
|
electronics: 180,
|
|
|
|
|
|
clothing: 220,
|
|
|
|
|
|
homeGarden: 280,
|
|
|
|
|
|
sports: 350,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 7)),
|
|
|
|
|
|
electronics: 420,
|
|
|
|
|
|
clothing: 550,
|
|
|
|
|
|
homeGarden: 380,
|
|
|
|
|
|
sports: 280,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 8)),
|
|
|
|
|
|
electronics: 280,
|
|
|
|
|
|
clothing: 380,
|
|
|
|
|
|
homeGarden: 520,
|
|
|
|
|
|
sports: 450,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 9)),
|
|
|
|
|
|
electronics: 550,
|
|
|
|
|
|
clothing: 280,
|
|
|
|
|
|
homeGarden: 420,
|
|
|
|
|
|
sports: 620,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 10)),
|
|
|
|
|
|
electronics: 323,
|
|
|
|
|
|
clothing: 729,
|
|
|
|
|
|
homeGarden: 506,
|
|
|
|
|
|
sports: 490,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 11)),
|
|
|
|
|
|
electronics: 480,
|
|
|
|
|
|
clothing: 420,
|
|
|
|
|
|
homeGarden: 280,
|
|
|
|
|
|
sports: 380,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: weekdayFormatter.format(new Date(2025, 0, 12)),
|
|
|
|
|
|
electronics: 220,
|
|
|
|
|
|
clothing: 550,
|
|
|
|
|
|
homeGarden: 450,
|
|
|
|
|
|
sports: 320,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const categoryMonthData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 1)),
|
|
|
|
|
|
electronics: 180,
|
|
|
|
|
|
clothing: 220,
|
|
|
|
|
|
homeGarden: 280,
|
|
|
|
|
|
sports: 350,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 5)),
|
|
|
|
|
|
electronics: 420,
|
|
|
|
|
|
clothing: 550,
|
|
|
|
|
|
homeGarden: 380,
|
|
|
|
|
|
sports: 280,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 9)),
|
|
|
|
|
|
electronics: 280,
|
|
|
|
|
|
clothing: 380,
|
|
|
|
|
|
homeGarden: 520,
|
|
|
|
|
|
sports: 450,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 13)),
|
|
|
|
|
|
electronics: 550,
|
|
|
|
|
|
clothing: 280,
|
|
|
|
|
|
homeGarden: 420,
|
|
|
|
|
|
sports: 620,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 15)),
|
|
|
|
|
|
electronics: 323,
|
|
|
|
|
|
clothing: 729,
|
|
|
|
|
|
homeGarden: 506,
|
|
|
|
|
|
sports: 490,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 17)),
|
|
|
|
|
|
electronics: 480,
|
|
|
|
|
|
clothing: 420,
|
|
|
|
|
|
homeGarden: 280,
|
|
|
|
|
|
sports: 380,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 21)),
|
|
|
|
|
|
electronics: 220,
|
|
|
|
|
|
clothing: 550,
|
|
|
|
|
|
homeGarden: 450,
|
|
|
|
|
|
sports: 320,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 25)),
|
|
|
|
|
|
electronics: 380,
|
|
|
|
|
|
clothing: 320,
|
|
|
|
|
|
homeGarden: 580,
|
|
|
|
|
|
sports: 480,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
day: monthDayFormatter.format(new Date(2025, 0, 30)),
|
|
|
|
|
|
electronics: 520,
|
|
|
|
|
|
clothing: 450,
|
|
|
|
|
|
homeGarden: 350,
|
|
|
|
|
|
sports: 420,
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const categoryChartConfig = {
|
|
|
|
|
|
electronics: { label: "الکترونیک", color: palette.primary },
|
|
|
|
|
|
clothing: {
|
|
|
|
|
|
label: "پوشاک",
|
|
|
|
|
|
theme: palette.secondary,
|
|
|
|
|
|
},
|
|
|
|
|
|
homeGarden: {
|
|
|
|
|
|
label: "خانه و باغ",
|
|
|
|
|
|
theme: palette.tertiary,
|
|
|
|
|
|
},
|
|
|
|
|
|
sports: {
|
|
|
|
|
|
label: "ورزشی",
|
|
|
|
|
|
theme: palette.quaternary,
|
|
|
|
|
|
},
|
|
|
|
|
|
} satisfies ChartConfig;
|
|
|
|
|
|
|
|
|
|
|
|
function getCategoryColor(
|
|
|
|
|
|
config: (typeof categoryChartConfig)[keyof typeof categoryChartConfig],
|
|
|
|
|
|
): string {
|
|
|
|
|
|
if ("color" in config && config.color) return config.color;
|
|
|
|
|
|
if ("theme" in config && config.theme) {
|
|
|
|
|
|
const theme = config.theme as { light: string; dark: string };
|
|
|
|
|
|
return theme.light;
|
|
|
|
|
|
}
|
|
|
|
|
|
return "var(--primary)";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 orderStatuses: OrderStatus[] = [
|
|
|
|
|
|
"Processing",
|
|
|
|
|
|
"Shipped",
|
|
|
|
|
|
"Delivered",
|
|
|
|
|
|
"Cancelled",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const orders: Order[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "1",
|
|
|
|
|
|
orderNumber: "ORD-2024-001",
|
|
|
|
|
|
customer: "Sarah Johnson",
|
|
|
|
|
|
customerInitials: "SJ",
|
|
|
|
|
|
products: ["MacBook Pro 14", "Magic Mouse"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 2499.0,
|
|
|
|
|
|
date: new Date(2024, 0, 28),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "2",
|
|
|
|
|
|
orderNumber: "ORD-2024-002",
|
|
|
|
|
|
customer: "Michael Chen",
|
|
|
|
|
|
customerInitials: "MC",
|
|
|
|
|
|
products: ["iPhone 15 Pro", "AirPods Pro"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Shipped",
|
|
|
|
|
|
total: 1348.0,
|
|
|
|
|
|
date: new Date(2024, 0, 27),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "3",
|
|
|
|
|
|
orderNumber: "ORD-2024-003",
|
|
|
|
|
|
customer: "Emma Wilson",
|
|
|
|
|
|
customerInitials: "EW",
|
|
|
|
|
|
products: ["iPad Air", "Apple Pencil", "Smart Keyboard"],
|
|
|
|
|
|
productCount: 3,
|
|
|
|
|
|
status: "Processing",
|
|
|
|
|
|
total: 1198.0,
|
|
|
|
|
|
date: new Date(2024, 0, 27),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "4",
|
|
|
|
|
|
orderNumber: "ORD-2024-004",
|
|
|
|
|
|
customer: "James Rodriguez",
|
|
|
|
|
|
customerInitials: "JR",
|
|
|
|
|
|
products: ["Apple Watch Ultra"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 799.0,
|
|
|
|
|
|
date: new Date(2024, 0, 26),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "5",
|
|
|
|
|
|
orderNumber: "ORD-2024-005",
|
|
|
|
|
|
customer: "Lisa Park",
|
|
|
|
|
|
customerInitials: "LP",
|
|
|
|
|
|
products: ["Mac Mini M2"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Cancelled",
|
|
|
|
|
|
total: 599.0,
|
|
|
|
|
|
date: new Date(2024, 0, 26),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "6",
|
|
|
|
|
|
orderNumber: "ORD-2024-006",
|
|
|
|
|
|
customer: "David Kim",
|
|
|
|
|
|
customerInitials: "DK",
|
|
|
|
|
|
products: ["Studio Display", "Mac Studio"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Shipped",
|
|
|
|
|
|
total: 5498.0,
|
|
|
|
|
|
date: new Date(2024, 0, 25),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "7",
|
|
|
|
|
|
orderNumber: "ORD-2024-007",
|
|
|
|
|
|
customer: "Anna Martinez",
|
|
|
|
|
|
customerInitials: "AM",
|
|
|
|
|
|
products: ["MacBook Air M2"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 1199.0,
|
|
|
|
|
|
date: new Date(2024, 0, 25),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "8",
|
|
|
|
|
|
orderNumber: "ORD-2024-008",
|
|
|
|
|
|
customer: "Robert Taylor",
|
|
|
|
|
|
customerInitials: "RT",
|
|
|
|
|
|
products: ["iPhone 15", "MagSafe Charger"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Processing",
|
|
|
|
|
|
total: 878.0,
|
|
|
|
|
|
date: new Date(2024, 0, 24),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "9",
|
|
|
|
|
|
orderNumber: "ORD-2024-009",
|
|
|
|
|
|
customer: "Jennifer Lee",
|
|
|
|
|
|
customerInitials: "JL",
|
|
|
|
|
|
products: ["AirPods Max"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Shipped",
|
|
|
|
|
|
total: 549.0,
|
|
|
|
|
|
date: new Date(2024, 0, 24),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "10",
|
|
|
|
|
|
orderNumber: "ORD-2024-010",
|
|
|
|
|
|
customer: "William Brown",
|
|
|
|
|
|
customerInitials: "WB",
|
|
|
|
|
|
products: ["iPad Pro 12.9", "Magic Keyboard"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 1648.0,
|
|
|
|
|
|
date: new Date(2024, 0, 23),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "11",
|
|
|
|
|
|
orderNumber: "ORD-2024-011",
|
|
|
|
|
|
customer: "Sophia Davis",
|
|
|
|
|
|
customerInitials: "SD",
|
|
|
|
|
|
products: ["MacBook Pro 16"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Processing",
|
|
|
|
|
|
total: 2499.0,
|
|
|
|
|
|
date: new Date(2024, 0, 23),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "12",
|
|
|
|
|
|
orderNumber: "ORD-2024-012",
|
|
|
|
|
|
customer: "Daniel Garcia",
|
|
|
|
|
|
customerInitials: "DG",
|
|
|
|
|
|
products: ["Apple TV 4K", "HomePod mini"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Cancelled",
|
|
|
|
|
|
total: 278.0,
|
|
|
|
|
|
date: new Date(2024, 0, 22),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "13",
|
|
|
|
|
|
orderNumber: "ORD-2024-013",
|
|
|
|
|
|
customer: "Olivia White",
|
|
|
|
|
|
customerInitials: "OW",
|
|
|
|
|
|
products: ["iPhone 15 Pro Max"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 1199.0,
|
|
|
|
|
|
date: new Date(2024, 0, 22),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "14",
|
|
|
|
|
|
orderNumber: "ORD-2024-014",
|
|
|
|
|
|
customer: "Thomas Anderson",
|
|
|
|
|
|
customerInitials: "TA",
|
|
|
|
|
|
products: ["Mac Pro", "Pro Display XDR"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Shipped",
|
|
|
|
|
|
total: 12498.0,
|
|
|
|
|
|
date: new Date(2024, 0, 21),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "15",
|
|
|
|
|
|
orderNumber: "ORD-2024-015",
|
|
|
|
|
|
customer: "Emily Thompson",
|
|
|
|
|
|
customerInitials: "ET",
|
|
|
|
|
|
products: ["iPad mini", "Apple Pencil"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 648.0,
|
|
|
|
|
|
date: new Date(2024, 0, 21),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "16",
|
|
|
|
|
|
orderNumber: "ORD-2024-016",
|
|
|
|
|
|
customer: "Christopher Moore",
|
|
|
|
|
|
customerInitials: "CM",
|
|
|
|
|
|
products: ["AirPods Pro 2"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Processing",
|
|
|
|
|
|
total: 249.0,
|
|
|
|
|
|
date: new Date(2024, 0, 20),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "17",
|
|
|
|
|
|
orderNumber: "ORD-2024-017",
|
|
|
|
|
|
customer: "Isabella Jackson",
|
|
|
|
|
|
customerInitials: "IJ",
|
|
|
|
|
|
products: ["Apple Watch Series 9"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Shipped",
|
|
|
|
|
|
total: 399.0,
|
|
|
|
|
|
date: new Date(2024, 0, 20),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "18",
|
|
|
|
|
|
orderNumber: "ORD-2024-018",
|
|
|
|
|
|
customer: "Matthew Harris",
|
|
|
|
|
|
customerInitials: "MH",
|
|
|
|
|
|
products: ["MacBook Air 15"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Delivered",
|
|
|
|
|
|
total: 1299.0,
|
|
|
|
|
|
date: new Date(2024, 0, 19),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "19",
|
|
|
|
|
|
orderNumber: "ORD-2024-019",
|
|
|
|
|
|
customer: "Ava Clark",
|
|
|
|
|
|
customerInitials: "AC",
|
|
|
|
|
|
products: ["iPhone SE", "EarPods"],
|
|
|
|
|
|
productCount: 2,
|
|
|
|
|
|
status: "Cancelled",
|
|
|
|
|
|
total: 448.0,
|
|
|
|
|
|
date: new Date(2024, 0, 19),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "20",
|
|
|
|
|
|
orderNumber: "ORD-2024-020",
|
|
|
|
|
|
customer: "Joshua Lewis",
|
|
|
|
|
|
customerInitials: "JL",
|
|
|
|
|
|
products: ["Mac Mini M2 Pro"],
|
|
|
|
|
|
productCount: 1,
|
|
|
|
|
|
status: "Processing",
|
|
|
|
|
|
total: 1299.0,
|
|
|
|
|
|
date: new Date(2024, 0, 18),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const PAGE_SIZE_OPTIONS = [10, 25, 50];
|
|
|
|
|
|
|
|
|
|
|
|
function readRecentOrdersTableState() {
|
|
|
|
|
|
const defaults = {
|
|
|
|
|
|
searchQuery: "",
|
|
|
|
|
|
statusFilter: "all",
|
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof window === "undefined") return defaults;
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
searchQuery: params.get("q") ?? "",
|
|
|
|
|
|
statusFilter:
|
|
|
|
|
|
statusParam && orderStatuses.includes(statusParam as OrderStatus)
|
|
|
|
|
|
? statusParam
|
|
|
|
|
|
: "all",
|
|
|
|
|
|
currentPage: Number.isFinite(pageParam) && pageParam > 0 ? pageParam : 1,
|
|
|
|
|
|
pageSize: PAGE_SIZE_OPTIONS.includes(pageSizeParam) ? pageSizeParam : 10,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
|
<LayoutDashboard className="size-5" aria-hidden="true" />
|
|
|
|
|
|
<h1 className="text-base font-medium text-pretty">داشبورد</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" aria-hidden="true" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
className="size-9"
|
|
|
|
|
|
aria-label="راهنما"
|
|
|
|
|
|
>
|
|
|
|
|
|
<HelpCircle className="size-4" aria-hidden="true" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const StatsCardsGrid = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
|
|
|
|
|
{statsGridData.map((stat) => {
|
|
|
|
|
|
const isPositive = stat.invertColor ? stat.change < 0 : stat.change > 0;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={stat.label}
|
|
|
|
|
|
className="flex flex-col rounded-xl border bg-card p-4 sm:p-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-xl font-semibold tracking-tight sm:text-3xl">
|
|
|
|
|
|
{formatStatValue(stat)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="mt-2 flex items-center gap-2">
|
|
|
|
|
|
<span className="text-xs text-muted-foreground sm:text-sm">
|
|
|
|
|
|
{stat.label}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-[10px] font-medium sm:text-xs",
|
|
|
|
|
|
isPositive
|
|
|
|
|
|
? "text-emerald-600 dark:text-emerald-400"
|
|
|
|
|
|
: "text-red-600 dark:text-red-400",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{stat.change > 0 ? "+" : ""}
|
|
|
|
|
|
{stat.change.toLocaleString("fa-IR", {
|
|
|
|
|
|
minimumFractionDigits: 1,
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
})}
|
|
|
|
|
|
٪
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const CategoryPerformanceChart = () => {
|
|
|
|
|
|
const subscribeToCategoryTab = React.useCallback((onStoreChange: () => void) => {
|
|
|
|
|
|
if (typeof window === "undefined") return () => {};
|
|
|
|
|
|
window.addEventListener("popstate", onStoreChange);
|
|
|
|
|
|
return () => window.removeEventListener("popstate", onStoreChange);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const readCategoryTabParam = React.useCallback((): "week" | "month" => {
|
|
|
|
|
|
if (typeof window === "undefined") return "week";
|
|
|
|
|
|
const nextTab = new URLSearchParams(window.location.search).get(
|
|
|
|
|
|
"categoryTab",
|
|
|
|
|
|
);
|
|
|
|
|
|
return nextTab === "month" ? "month" : "week";
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const tab = React.useSyncExternalStore(
|
|
|
|
|
|
subscribeToCategoryTab,
|
|
|
|
|
|
readCategoryTabParam,
|
|
|
|
|
|
() => "week" as const,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const setTab = React.useCallback((next: "week" | "month") => {
|
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
if (next !== "week") {
|
|
|
|
|
|
params.set("categoryTab", next);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params.delete("categoryTab");
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextQuery = params.toString();
|
|
|
|
|
|
const nextUrl = nextQuery
|
|
|
|
|
|
? `${window.location.pathname}?${nextQuery}`
|
|
|
|
|
|
: window.location.pathname;
|
|
|
|
|
|
window.history.replaceState(null, "", nextUrl);
|
|
|
|
|
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const chartData = tab === "week" ? categoryWeekData : categoryMonthData;
|
|
|
|
|
|
const totalOrders = chartData.reduce(
|
|
|
|
|
|
(acc, item) =>
|
|
|
|
|
|
acc + item.electronics + item.clothing + item.homeGarden + item.sports,
|
|
|
|
|
|
0,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col gap-4 rounded-xl border bg-card p-5">
|
|
|
|
|
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
|
<h2 className="text-sm font-medium text-pretty">
|
|
|
|
|
|
عملکرد دستهها
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<span className="text-xl font-bold tracking-tight">
|
|
|
|
|
|
{numberFormatter.format(totalOrders)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
|
|
|
|
{Object.entries(categoryChartConfig).map(([key, config]) => (
|
|
|
|
|
|
<div key={key} className="flex items-center gap-1.5">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="size-2 rounded-full"
|
|
|
|
|
|
style={{ backgroundColor: getCategoryColor(config) }}
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">
|
|
|
|
|
|
{config.label}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center gap-0.5 rounded-lg border bg-muted/50 p-0.5"
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
aria-label="بازه زمانی عملکرد دستهها"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setTab("week")}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"rounded-md px-3 py-1 text-xs font-medium transition-colors",
|
|
|
|
|
|
tab === "week"
|
|
|
|
|
|
? "bg-background text-foreground shadow-sm"
|
|
|
|
|
|
: "text-muted-foreground hover:text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
aria-label="نمایش هفتگی"
|
|
|
|
|
|
>
|
|
|
|
|
|
هفته
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setTab("month")}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"rounded-md px-3 py-1 text-xs font-medium transition-colors",
|
|
|
|
|
|
tab === "month"
|
|
|
|
|
|
? "bg-background text-foreground shadow-sm"
|
|
|
|
|
|
: "text-muted-foreground hover:text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
aria-label="نمایش ماهانه"
|
|
|
|
|
|
>
|
|
|
|
|
|
ماه
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
id="category-chart-panel"
|
|
|
|
|
|
className="h-[240px] w-full min-w-0 sm:h-[280px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChartContainer config={categoryChartConfig} className="h-full w-full">
|
|
|
|
|
|
<LineChart data={chartData}>
|
|
|
|
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
|
|
|
|
<XAxis
|
|
|
|
|
|
dataKey="day"
|
|
|
|
|
|
axisLine={false}
|
|
|
|
|
|
tickLine={false}
|
|
|
|
|
|
tick={{ fontSize: 10 }}
|
|
|
|
|
|
dy={8}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<YAxis
|
|
|
|
|
|
axisLine={false}
|
|
|
|
|
|
tickLine={false}
|
|
|
|
|
|
tick={{ fontSize: 10 }}
|
|
|
|
|
|
dx={-5}
|
|
|
|
|
|
domain={[0, 1000]}
|
|
|
|
|
|
width={32}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
content={({ active, payload, label }) => {
|
|
|
|
|
|
if (!active || !payload?.length) return null;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-border bg-popover p-2 shadow-lg">
|
|
|
|
|
|
<p className="mb-1 text-xs font-medium text-foreground">
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{payload.map((entry) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`${entry.dataKey ?? entry.name ?? "segment"}`}
|
|
|
|
|
|
className="flex items-center gap-1.5 text-[10px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="size-2 rounded-full"
|
|
|
|
|
|
style={{ backgroundColor: entry.color }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
|
{categoryChartConfig[
|
|
|
|
|
|
entry.dataKey as keyof typeof categoryChartConfig
|
|
|
|
|
|
]?.label ?? entry.dataKey}
|
|
|
|
|
|
:
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="font-medium text-foreground">
|
|
|
|
|
|
{numberFormatter.format(Number(entry.value))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Line
|
|
|
|
|
|
type="linear"
|
|
|
|
|
|
dataKey="electronics"
|
|
|
|
|
|
stroke="var(--color-electronics)"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
dot={false}
|
|
|
|
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Line
|
|
|
|
|
|
type="linear"
|
|
|
|
|
|
dataKey="clothing"
|
|
|
|
|
|
stroke="var(--color-clothing)"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
dot={false}
|
|
|
|
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Line
|
|
|
|
|
|
type="linear"
|
|
|
|
|
|
dataKey="homeGarden"
|
|
|
|
|
|
stroke="var(--color-homeGarden)"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
dot={false}
|
|
|
|
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Line
|
|
|
|
|
|
type="linear"
|
|
|
|
|
|
dataKey="sports"
|
|
|
|
|
|
stroke="var(--color-sports)"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
dot={false}
|
|
|
|
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</LineChart>
|
|
|
|
|
|
</ChartContainer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const FulfillmentPanel = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-4 rounded-xl border bg-card p-5 xl:w-[420px]">
|
|
|
|
|
|
<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="w-24 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>
|
|
|
|
|
|
<span className="w-24 shrink-0 text-muted-foreground">
|
|
|
|
|
|
{dateFormatter.format(row.shipped)}
|
|
|
|
|
|
</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 w-1.5 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 tableHeadClass = "text-xs font-medium text-muted-foreground sm:text-sm";
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
|
() => readRecentOrdersTableState().searchQuery,
|
|
|
|
|
|
);
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = React.useState<string>(
|
|
|
|
|
|
() => readRecentOrdersTableState().statusFilter,
|
|
|
|
|
|
);
|
|
|
|
|
|
const [currentPage, setCurrentPage] = React.useState(
|
|
|
|
|
|
() => readRecentOrdersTableState().currentPage,
|
|
|
|
|
|
);
|
|
|
|
|
|
const [pageSize, setPageSize] = React.useState(
|
|
|
|
|
|
() => readRecentOrdersTableState().pageSize,
|
|
|
|
|
|
);
|
|
|
|
|
|
const isHydrated = typeof window !== "undefined";
|
|
|
|
|
|
const skipNextReset = React.useRef(true);
|
|
|
|
|
|
|
|
|
|
|
|
const hasActiveFilters = statusFilter !== "all";
|
|
|
|
|
|
|
|
|
|
|
|
const clearFilters = () => {
|
|
|
|
|
|
setStatusFilter("all");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const filteredOrders = React.useMemo(() => {
|
|
|
|
|
|
return orders.filter((order) => {
|
|
|
|
|
|
const matchesSearch =
|
|
|
|
|
|
order.orderNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
|
|
|
|
order.customer.toLowerCase().includes(searchQuery.toLowerCase());
|
|
|
|
|
|
|
|
|
|
|
|
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 (!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]"
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<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="جستجوی سفارش… (مثلاً سفارش-۱۴۰۳-002)"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => 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" aria-hidden="true" />
|
|
|
|
|
|
</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={cn("w-[40px]", tableHeadClass)}>
|
|
|
|
|
|
#
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className={cn("min-w-[160px]", tableHeadClass)}>
|
|
|
|
|
|
سفارش
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"hidden min-w-[140px] md:table-cell",
|
|
|
|
|
|
tableHeadClass,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
مشتری
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className={cn("min-w-[100px]", tableHeadClass)}>
|
|
|
|
|
|
وضعیت
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className={cn("min-w-[90px]", tableHeadClass)}>
|
|
|
|
|
|
مجموع
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className={cn("hidden sm:table-cell", tableHeadClass)}>
|
|
|
|
|
|
تاریخ
|
|
|
|
|
|
</TableHead>
|
|
|
|
|
|
<TableHead className="w-[40px]"></TableHead>
|
|
|
|
|
|
</TableRow>
|
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
|
{paginatedOrders.length === 0 ? (
|
|
|
|
|
|
<TableRow>
|
|
|
|
|
|
<TableCell
|
|
|
|
|
|
colSpan={7}
|
|
|
|
|
|
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 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">
|
|
|
|
|
|
{dateFormatter.format(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"
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</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.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) => 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" aria-hidden="true" />
|
|
|
|
|
|
</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" aria-hidden="true" />
|
|
|
|
|
|
</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)}
|
|
|
|
|
|
aria-label={`رفتن به صفحه ${pageNum.toLocaleString("fa-IR")}`}
|
|
|
|
|
|
aria-current={currentPage === pageNum ? "page" : undefined}
|
|
|
|
|
|
>
|
|
|
|
|
|
{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" aria-hidden="true" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
className="size-8"
|
|
|
|
|
|
onClick={() => goToPage(totalPages)}
|
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
|
aria-label="رفتن به صفحه آخر"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChevronsRight className="size-4 rtl:rotate-180" aria-hidden="true" />
|
|
|
|
|
|
</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:p-4 md:p-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<StatsCardsGrid />
|
|
|
|
|
|
<div className="flex flex-col gap-4 xl:flex-row">
|
|
|
|
|
|
<CategoryPerformanceChart />
|
|
|
|
|
|
<FulfillmentPanel />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<RecentOrdersTable />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const Dashboard7 = ({ 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 { Dashboard7 };
|