2026-07-11 21:21:29 +03:30
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
Activity,
|
|
|
|
|
|
BarChart3,
|
|
|
|
|
|
Bell,
|
|
|
|
|
|
Building2,
|
|
|
|
|
|
ChevronLeft,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
ChevronsUpDown,
|
|
|
|
|
|
CircleDollarSign,
|
|
|
|
|
|
CreditCard,
|
|
|
|
|
|
Download,
|
|
|
|
|
|
FileText,
|
|
|
|
|
|
LayoutDashboard,
|
|
|
|
|
|
LogOut,
|
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
|
Receipt,
|
|
|
|
|
|
Search,
|
|
|
|
|
|
Settings,
|
|
|
|
|
|
User,
|
|
|
|
|
|
UserPlus,
|
|
|
|
|
|
Users,
|
|
|
|
|
|
Zap,
|
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
import * as React from "react";
|
|
|
|
|
|
import type { TooltipProps } from "recharts";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Area,
|
|
|
|
|
|
AreaChart,
|
|
|
|
|
|
Bar,
|
|
|
|
|
|
BarChart,
|
|
|
|
|
|
CartesianGrid,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
XAxis,
|
|
|
|
|
|
YAxis,
|
|
|
|
|
|
} from "recharts";
|
|
|
|
|
|
|
|
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Collapsible,
|
|
|
|
|
|
CollapsibleContent,
|
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
|
} from "@/components/ui/collapsible";
|
|
|
|
|
|
import {
|
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
|
DropdownMenuCheckboxItem,
|
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
|
DropdownMenuLabel,
|
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Sidebar,
|
|
|
|
|
|
SidebarContent,
|
|
|
|
|
|
SidebarFooter,
|
|
|
|
|
|
SidebarGroup,
|
|
|
|
|
|
SidebarGroupContent,
|
|
|
|
|
|
SidebarGroupLabel,
|
|
|
|
|
|
SidebarHeader,
|
|
|
|
|
|
SidebarInset,
|
|
|
|
|
|
SidebarMenu,
|
|
|
|
|
|
SidebarMenuButton,
|
|
|
|
|
|
SidebarMenuItem,
|
|
|
|
|
|
SidebarMenuSub,
|
|
|
|
|
|
SidebarMenuSubButton,
|
|
|
|
|
|
SidebarMenuSubItem,
|
|
|
|
|
|
SidebarProvider,
|
|
|
|
|
|
SidebarRail,
|
|
|
|
|
|
SidebarTrigger,
|
|
|
|
|
|
} from "@/components/ui/sidebar";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
TableBody,
|
|
|
|
|
|
TableCell,
|
|
|
|
|
|
TableHead,
|
|
|
|
|
|
TableHeader,
|
|
|
|
|
|
TableRow,
|
|
|
|
|
|
} from "@/components/ui/table";
|
|
|
|
|
|
import { 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;
|
|
|
|
|
|
href: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
navGroups: NavGroup[];
|
|
|
|
|
|
footerGroup: NavGroup;
|
|
|
|
|
|
user?: UserData;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type TimePeriod = "6months" | "year";
|
|
|
|
|
|
|
|
|
|
|
|
type StatCardItem = {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
value: number;
|
|
|
|
|
|
format: "currency" | "number";
|
|
|
|
|
|
changePercent: number;
|
|
|
|
|
|
isPositive: boolean;
|
|
|
|
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
|
|
|
|
sparklineData: number[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type OrderStatus = "Processing" | "Shipped" | "Delivered" | "Cancelled";
|
|
|
|
|
|
|
|
|
|
|
|
type Order = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
orderNumber: string;
|
|
|
|
|
|
customer: string;
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
status: OrderStatus;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
type ActivityItem = {
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
detail: string;
|
|
|
|
|
|
minutesAgo: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
style: "currency",
|
|
|
|
|
|
currency: "IRR",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
|
|
|
|
|
|
style: "currency",
|
|
|
|
|
|
currency: "IRR",
|
|
|
|
|
|
notation: "compact",
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const numberFormatter = new Intl.NumberFormat("fa-IR");
|
|
|
|
|
|
const relativeTimeFormatter = new Intl.RelativeTimeFormat("fa-IR", {
|
|
|
|
|
|
numeric: "auto",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const monthLabel = (monthIndex: number) =>
|
|
|
|
|
|
new Intl.DateTimeFormat("fa-IR", { month: "short" }).format(
|
|
|
|
|
|
new Date(2025, monthIndex, 1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const orderStatusLabels: Record<OrderStatus, string> = {
|
|
|
|
|
|
Processing: "در حال پردازش",
|
|
|
|
|
|
Shipped: "ارسالشده",
|
|
|
|
|
|
Delivered: "تحویلشده",
|
|
|
|
|
|
Cancelled: "لغوشده",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatRelativeTime = (minutesAgo: number) => {
|
|
|
|
|
|
if (minutesAgo < 60) {
|
|
|
|
|
|
return relativeTimeFormatter.format(-minutesAgo, "minute");
|
|
|
|
|
|
}
|
|
|
|
|
|
const hours = Math.round(minutesAgo / 60);
|
|
|
|
|
|
if (hours < 24) {
|
|
|
|
|
|
return relativeTimeFormatter.format(-hours, "hour");
|
|
|
|
|
|
}
|
|
|
|
|
|
const days = Math.round(hours / 24);
|
|
|
|
|
|
return relativeTimeFormatter.format(-days, "day");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 revenueChartConfig = {
|
|
|
|
|
|
thisYear: { label: "امسال", color: palette.primary },
|
|
|
|
|
|
prevYear: { label: "سال قبل", theme: palette.secondary },
|
|
|
|
|
|
} satisfies ChartConfig;
|
|
|
|
|
|
|
|
|
|
|
|
const sparklinePositiveConfig = {
|
|
|
|
|
|
value: {
|
|
|
|
|
|
label: "مقدار",
|
|
|
|
|
|
theme: {
|
|
|
|
|
|
light: "oklch(0.55 0.18 155)",
|
|
|
|
|
|
dark: "oklch(0.72 0.15 155)",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
} satisfies ChartConfig;
|
|
|
|
|
|
|
|
|
|
|
|
const sparklineNegativeConfig = {
|
|
|
|
|
|
value: {
|
|
|
|
|
|
label: "مقدار",
|
|
|
|
|
|
theme: {
|
|
|
|
|
|
light: "oklch(0.55 0.18 25)",
|
|
|
|
|
|
dark: "oklch(0.72 0.15 25)",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
} satisfies ChartConfig;
|
|
|
|
|
|
|
|
|
|
|
|
const sidebarData: SidebarData = {
|
|
|
|
|
|
logo: {
|
2026-07-15 20:22:14 +02:00
|
|
|
|
src: "/block-media/shadcnblocks/block/logos/NovaSales-logo.svg",
|
|
|
|
|
|
alt: "نوافروش",
|
|
|
|
|
|
title: "NovaSales",
|
|
|
|
|
|
description: "پلتفرم فروش ابری",
|
|
|
|
|
|
href: "https://www.NovaSales",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
},
|
|
|
|
|
|
navGroups: [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "نمای کلی",
|
|
|
|
|
|
defaultOpen: true,
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: "داشبورد",
|
|
|
|
|
|
icon: LayoutDashboard,
|
|
|
|
|
|
href: "#",
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
},
|
2026-07-15 20:22:14 +02:00
|
|
|
|
{ label: "تحلیل فروش", icon: BarChart3, href: "#" },
|
|
|
|
|
|
{ label: "فعالیت تیم", icon: Activity, href: "#" },
|
2026-07-11 21:21:29 +03:30
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
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: "فهرست معاملات",
|
2026-07-11 21:21:29 +03:30
|
|
|
|
icon: Users,
|
|
|
|
|
|
href: "#",
|
|
|
|
|
|
children: [
|
2026-07-15 20:22:14 +02:00
|
|
|
|
{ label: "همه اعضای تیم", icon: User, href: "#" },
|
2026-07-11 21:21:29 +03:30
|
|
|
|
{ label: "فضاهای کاری", icon: Building2, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{ label: "دعوتها", icon: UserPlus, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "صورتحساب",
|
|
|
|
|
|
defaultOpen: false,
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{ label: "اشتراکها", icon: CreditCard, href: "#" },
|
|
|
|
|
|
{ label: "مصرف", icon: Zap, href: "#" },
|
|
|
|
|
|
{ label: "فاکتورها", icon: Receipt, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "گزارشها",
|
|
|
|
|
|
defaultOpen: false,
|
|
|
|
|
|
items: [
|
|
|
|
|
|
{ label: "گزارشهای ذخیرهشده", icon: FileText, href: "#" },
|
|
|
|
|
|
{ label: "خروجیها", icon: Download, href: "#" },
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
footerGroup: {
|
|
|
|
|
|
title: "تنظیمات",
|
|
|
|
|
|
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
|
|
|
|
|
|
},
|
|
|
|
|
|
user: {
|
|
|
|
|
|
name: "مریم الیس",
|
|
|
|
|
|
email: "morgan.ellis@northwind.app",
|
|
|
|
|
|
avatar:
|
|
|
|
|
|
"/block-media/shadcnblocks/block/avatar-1.webp",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const headerNotificationItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "1",
|
|
|
|
|
|
title: "افزایش مصرف",
|
|
|
|
|
|
body: "حجم API در فضای کاری شما ۲۴٪ بالاتر از میانگین ۷ روز است.",
|
|
|
|
|
|
time: "۶ دقیقه پیش",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "2",
|
|
|
|
|
|
title: "فاکتور پرداخت شد",
|
|
|
|
|
|
body: "نورثویند فاکتور INV-2041 را (۱۲٬۸۰۰ دلار) با ACH پرداخت کرد.",
|
|
|
|
|
|
time: "۴۲ دقیقه پیش",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "3",
|
|
|
|
|
|
title: "تمدید نزدیک است",
|
|
|
|
|
|
body: "۲ قرارداد سالانه ظرف ۱۴ روز آینده تمدید میشوند.",
|
|
|
|
|
|
time: "۲ ساعت پیش",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "4",
|
|
|
|
|
|
title: "خلاصه هفتگی",
|
|
|
|
|
|
body: "خلاصه درآمد و ریزش برای تیم شما آماده است.",
|
|
|
|
|
|
time: "دیروز",
|
|
|
|
|
|
},
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
|
|
const unreadNotificationCount = headerNotificationItems.length;
|
|
|
|
|
|
|
|
|
|
|
|
const fullYearData = [
|
|
|
|
|
|
{ month: monthLabel(0), thisYear: 42000, prevYear: 38000 },
|
|
|
|
|
|
{ month: monthLabel(1), thisYear: 38000, prevYear: 45000 },
|
|
|
|
|
|
{ month: monthLabel(2), thisYear: 52000, prevYear: 41000 },
|
|
|
|
|
|
{ month: monthLabel(3), thisYear: 45000, prevYear: 48000 },
|
|
|
|
|
|
{ month: monthLabel(4), thisYear: 58000, prevYear: 44000 },
|
|
|
|
|
|
{ month: monthLabel(5), thisYear: 41000, prevYear: 52000 },
|
|
|
|
|
|
{ month: monthLabel(6), thisYear: 55000, prevYear: 47000 },
|
|
|
|
|
|
{ month: monthLabel(7), thisYear: 48000, prevYear: 53000 },
|
|
|
|
|
|
{ month: monthLabel(8), thisYear: 62000, prevYear: 49000 },
|
|
|
|
|
|
{ month: monthLabel(9), thisYear: 54000, prevYear: 58000 },
|
|
|
|
|
|
{ month: monthLabel(10), thisYear: 67000, prevYear: 52000 },
|
|
|
|
|
|
{ month: monthLabel(11), thisYear: 71000, prevYear: 61000 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const periodLabels: Record<TimePeriod, string> = {
|
|
|
|
|
|
"6months": "۶ ماه اخیر",
|
|
|
|
|
|
year: "۱۲ ماه",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function getDataForPeriod(period: TimePeriod) {
|
|
|
|
|
|
if (period === "6months") return fullYearData.slice(0, 6);
|
|
|
|
|
|
return fullYearData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const statsCardData: StatCardItem[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "درآمد خالص",
|
|
|
|
|
|
value: 82450,
|
|
|
|
|
|
format: "currency",
|
|
|
|
|
|
changePercent: 20,
|
|
|
|
|
|
isPositive: true,
|
|
|
|
|
|
icon: CircleDollarSign,
|
|
|
|
|
|
sparklineData: [32, 40, 35, 50, 49, 60, 70, 65, 72, 80],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "حسابهای فعال",
|
|
|
|
|
|
value: 3670,
|
|
|
|
|
|
format: "number",
|
|
|
|
|
|
changePercent: -4.2,
|
|
|
|
|
|
isPositive: false,
|
|
|
|
|
|
icon: Users,
|
|
|
|
|
|
sparklineData: [45, 42, 48, 40, 38, 36, 34, 35, 33, 32],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "ARPU",
|
|
|
|
|
|
value: 56.2,
|
|
|
|
|
|
format: "currency",
|
|
|
|
|
|
changePercent: 4.5,
|
|
|
|
|
|
isPositive: true,
|
|
|
|
|
|
icon: CreditCard,
|
|
|
|
|
|
sparklineData: [20, 25, 22, 30, 28, 35, 40, 38, 42, 45],
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const orderStatuses: OrderStatus[] = [
|
|
|
|
|
|
"Processing",
|
|
|
|
|
|
"Shipped",
|
|
|
|
|
|
"Delivered",
|
|
|
|
|
|
"Cancelled",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
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 tableHeadClassName =
|
|
|
|
|
|
"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 recentActivity: ActivityItem[] = [
|
|
|
|
|
|
{ title: "ثبتنام کاربر جدید", detail: "الکس مورگان", minutesAgo: 2 },
|
|
|
|
|
|
{ title: "ایجاد فضای کاری", detail: "نورثویند لبز", minutesAgo: 2 },
|
|
|
|
|
|
{ title: "دریافت پرداخت", detail: "INV-2044 · ۴٬۲۰۰ دلار", minutesAgo: 2 },
|
|
|
|
|
|
{ title: "ارسال فاکتور", detail: "جیمی لی", minutesAgo: 6 },
|
|
|
|
|
|
{ title: "ارتقای پلن", detail: "نوح رابرتس", minutesAgo: 12 },
|
|
|
|
|
|
{ title: "ثبت اعتبار", detail: "الیویا وایت", minutesAgo: 18 },
|
|
|
|
|
|
{ title: "بازنشانی رمز", detail: "ایتن پارکر", minutesAgo: 24 },
|
|
|
|
|
|
{ title: "نظر روی گزارش", detail: "سوفیا دیویس", minutesAgo: 32 },
|
|
|
|
|
|
{ title: "بستن تیکت پشتیبانی", detail: "مایکل چن", minutesAgo: 45 },
|
|
|
|
|
|
{ title: "پردازش تسویه", detail: "اما ویلسون", minutesAgo: 60 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const SidebarLogo = ({ logo }: { logo: SidebarData["logo"] }) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SidebarMenu>
|
|
|
|
|
|
<SidebarMenuItem>
|
|
|
|
|
|
<SidebarMenuButton size="lg" tooltip={logo.title} asChild>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={logo.href}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
|
className="min-w-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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 min-w-0 flex-col gap-0.5 leading-none">
|
|
|
|
|
|
<span className="truncate font-medium">{logo.title}</span>
|
|
|
|
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
|
|
|
|
{logo.description}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</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="جستجوی داشبورد"
|
2026-07-15 20:22:14 +02:00
|
|
|
|
placeholder="جستجوی اعضای تیم، فضاهای کاری…"
|
2026-07-11 21:21:29 +03:30
|
|
|
|
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>
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="icon"
|
|
|
|
|
|
className="relative size-9"
|
|
|
|
|
|
aria-label={`اعلانها، ${unreadNotificationCount.toLocaleString("fa-IR")} خواندهنشده`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Bell className="size-4" aria-hidden="true" />
|
|
|
|
|
|
<span className="absolute -top-1 -end-1 flex size-[18px] items-center justify-center rounded-full bg-red-600 text-[10px] leading-none font-semibold text-white ring-2 ring-background">
|
|
|
|
|
|
{unreadNotificationCount}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
<DropdownMenuContent align="end" className="w-80 sm:w-96">
|
|
|
|
|
|
<DropdownMenuLabel className="text-base font-medium">
|
|
|
|
|
|
اعلانها
|
|
|
|
|
|
</DropdownMenuLabel>
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
{headerNotificationItems.map((item) => (
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
key={item.id}
|
|
|
|
|
|
className="cursor-default flex-col items-start gap-1 py-3"
|
|
|
|
|
|
onSelect={(event: Event) => event.preventDefault()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="font-medium text-foreground">
|
|
|
|
|
|
{item.title}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs leading-snug font-normal text-muted-foreground">
|
|
|
|
|
|
{item.body}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground/80">
|
|
|
|
|
|
{item.time}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuItem className="justify-center text-center font-medium">
|
|
|
|
|
|
علامتگذاری همه بهعنوان خواندهشده
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function CustomTooltip({
|
|
|
|
|
|
active,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
label,
|
|
|
|
|
|
colors,
|
|
|
|
|
|
}: any & {
|
|
|
|
|
|
colors: { primary: string; secondary: string };
|
|
|
|
|
|
}) {
|
|
|
|
|
|
if (!active || !payload?.length) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const thisYear = payload.find((p: any) => p.dataKey === "thisYear")?.value || 0;
|
|
|
|
|
|
const prevYear = payload.find((p: any) => p.dataKey === "prevYear")?.value || 0;
|
|
|
|
|
|
const diff = Number(thisYear) - Number(prevYear);
|
|
|
|
|
|
const percentage = prevYear ? Math.round((diff / Number(prevYear)) * 100) : 0;
|
|
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-border bg-popover p-2 shadow-lg sm:p-3">
|
|
|
|
|
|
<p className="mb-1.5 text-xs font-medium text-foreground sm:mb-2 sm:text-sm">
|
|
|
|
|
|
{label}، {currentYear.toLocaleString("fa-IR")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="space-y-1 sm:space-y-1.5">
|
|
|
|
|
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="size-2 rounded-full sm:size-2.5"
|
|
|
|
|
|
style={{ backgroundColor: colors.primary }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground sm:text-sm">
|
|
|
|
|
|
امسال:
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-[10px] font-medium text-foreground sm:text-sm">
|
|
|
|
|
|
{currencyFormatter.format(Number(thisYear))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="size-2 rounded-full sm:size-2.5"
|
|
|
|
|
|
style={{ backgroundColor: colors.secondary }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-[10px] text-muted-foreground sm:text-sm">
|
|
|
|
|
|
سال قبل:
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-[10px] font-medium text-foreground sm:text-sm">
|
|
|
|
|
|
{currencyFormatter.format(Number(prevYear))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-1 border-t border-border pt-1">
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-[10px] font-medium sm:text-xs",
|
|
|
|
|
|
diff >= 0 ? "text-emerald-500" : "text-red-500",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{diff >= 0 ? "+" : ""}
|
|
|
|
|
|
{percentage.toLocaleString("fa-IR")}٪ نسبت به سال قبل
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const RevenueFlowChart = () => {
|
|
|
|
|
|
const [period, setPeriod] = React.useState<TimePeriod>("6months");
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const nextPeriod = params.get("period");
|
|
|
|
|
|
if (nextPeriod === "6months" || nextPeriod === "year") {
|
|
|
|
|
|
setPeriod(nextPeriod);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
if (period !== "6months") {
|
|
|
|
|
|
params.set("period", period);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params.delete("period");
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextQuery = params.toString();
|
|
|
|
|
|
const nextUrl = nextQuery
|
|
|
|
|
|
? `${window.location.pathname}?${nextQuery}`
|
|
|
|
|
|
: window.location.pathname;
|
|
|
|
|
|
window.history.replaceState(null, "", nextUrl);
|
|
|
|
|
|
}, [period]);
|
|
|
|
|
|
|
|
|
|
|
|
const chartData = getDataForPeriod(period);
|
|
|
|
|
|
const 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>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
onSelect={(event: Event) => event.preventDefault()}
|
|
|
|
|
|
>
|
|
|
|
|
|
خروجی داده نمودار
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
onSelect={(event: Event) => event.preventDefault()}
|
|
|
|
|
|
>
|
|
|
|
|
|
باز کردن در گزارشگیری
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="h-[200px] w-full min-w-0 sm:h-[240px] lg:h-[280px]">
|
|
|
|
|
|
<ChartContainer config={revenueChartConfig} className="h-full w-full">
|
|
|
|
|
|
<BarChart data={chartData} barGap={2}>
|
|
|
|
|
|
<CartesianGrid strokeDasharray="0" vertical={false} />
|
|
|
|
|
|
<XAxis
|
|
|
|
|
|
dataKey="month"
|
|
|
|
|
|
axisLine={false}
|
|
|
|
|
|
tickLine={false}
|
|
|
|
|
|
tick={{ fontSize: 10 }}
|
|
|
|
|
|
dy={8}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<YAxis
|
|
|
|
|
|
axisLine={false}
|
|
|
|
|
|
tickLine={false}
|
|
|
|
|
|
tick={{ fontSize: 10 }}
|
|
|
|
|
|
dx={-5}
|
|
|
|
|
|
tickFormatter={(value) => compactCurrencyFormatter.format(value)}
|
|
|
|
|
|
width={40}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
content={
|
|
|
|
|
|
<CustomTooltip
|
|
|
|
|
|
colors={{
|
|
|
|
|
|
primary: "var(--color-thisYear)",
|
|
|
|
|
|
secondary: "var(--color-prevYear)",
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
cursor={{ fillOpacity: 0.05, radius: 4 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Bar
|
|
|
|
|
|
dataKey="thisYear"
|
|
|
|
|
|
fill="var(--color-thisYear)"
|
|
|
|
|
|
radius={[4, 4, 0, 0]}
|
|
|
|
|
|
maxBarSize={24}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Bar
|
|
|
|
|
|
dataKey="prevYear"
|
|
|
|
|
|
fill="var(--color-prevYear)"
|
|
|
|
|
|
fillOpacity={0.4}
|
|
|
|
|
|
radius={[4, 4, 0, 0]}
|
|
|
|
|
|
maxBarSize={24}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</BarChart>
|
|
|
|
|
|
</ChartContainer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const StatsCardsVertical = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:flex xl:w-[360px] xl:flex-col xl:justify-between">
|
|
|
|
|
|
{statsCardData.map((stat) => {
|
|
|
|
|
|
const sparkData = stat.sparklineData.map((v, i) => ({
|
|
|
|
|
|
idx: i,
|
|
|
|
|
|
value: v,
|
|
|
|
|
|
}));
|
|
|
|
|
|
const chartConfig = stat.isPositive
|
|
|
|
|
|
? sparklinePositiveConfig
|
|
|
|
|
|
: sparklineNegativeConfig;
|
|
|
|
|
|
const sparkColor = stat.isPositive ? "#16a34a" : "#dc2626";
|
|
|
|
|
|
const gradientId = `sparkFill-${stat.title.replace(/\s+/g, "-")}`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={stat.title}
|
|
|
|
|
|
className="flex flex-col gap-3 rounded-xl border bg-card p-4 sm:p-5"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex flex-col gap-1">
|
|
|
|
|
|
<span className="text-xs text-muted-foreground sm:text-sm">
|
|
|
|
|
|
{stat.title}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-lg font-semibold tracking-tight sm:text-xl">
|
|
|
|
|
|
{stat.format === "currency"
|
|
|
|
|
|
? currencyFormatter.format(stat.value)
|
|
|
|
|
|
: numberFormatter.format(stat.value)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="h-10 w-24">
|
|
|
|
|
|
<ChartContainer config={chartConfig} className="h-full w-full">
|
|
|
|
|
|
<AreaChart data={sparkData}>
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<linearGradient
|
|
|
|
|
|
id={gradientId}
|
|
|
|
|
|
x1="0"
|
|
|
|
|
|
y1="0"
|
|
|
|
|
|
x2="0"
|
|
|
|
|
|
y2="1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<stop
|
|
|
|
|
|
offset="0%"
|
|
|
|
|
|
stopColor={sparkColor}
|
|
|
|
|
|
stopOpacity={0.25}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<stop
|
|
|
|
|
|
offset="100%"
|
|
|
|
|
|
stopColor={sparkColor}
|
|
|
|
|
|
stopOpacity={0.02}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
content={({ active, payload }) => {
|
|
|
|
|
|
if (!active || !payload?.length) return null;
|
|
|
|
|
|
const value = payload[0].value as number;
|
|
|
|
|
|
const formatted =
|
|
|
|
|
|
stat.format === "currency"
|
|
|
|
|
|
? currencyFormatter.format(value)
|
|
|
|
|
|
: numberFormatter.format(value);
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-md border border-border bg-popover px-2 py-1 shadow-lg">
|
|
|
|
|
|
<p className="text-xs font-medium text-foreground">
|
|
|
|
|
|
{formatted}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
cursor={{ strokeOpacity: 0.2 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Area
|
|
|
|
|
|
type="monotone"
|
|
|
|
|
|
dataKey="value"
|
|
|
|
|
|
stroke={sparkColor}
|
|
|
|
|
|
strokeWidth={1.5}
|
|
|
|
|
|
fill={`url(#${gradientId})`}
|
|
|
|
|
|
dot={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</AreaChart>
|
|
|
|
|
|
</ChartContainer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 text-[10px] sm:text-xs">
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"font-medium",
|
|
|
|
|
|
stat.isPositive
|
|
|
|
|
|
? "text-emerald-600 dark:text-emerald-400"
|
|
|
|
|
|
: "text-red-600 dark:text-red-400",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{stat.isPositive ? "+" : ""}
|
|
|
|
|
|
{stat.changePercent.toLocaleString("fa-IR", {
|
|
|
|
|
|
minimumFractionDigits: 1,
|
|
|
|
|
|
maximumFractionDigits: 1,
|
|
|
|
|
|
})}
|
|
|
|
|
|
٪
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="flex items-center gap-2 text-muted-foreground">
|
|
|
|
|
|
<span className="size-1 rounded-full bg-muted-foreground" />
|
|
|
|
|
|
<span>نسبت به ماه قبل</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const RecentTransactionsTable = ({ className }: { className?: string }) => {
|
|
|
|
|
|
const [statusFilter, setStatusFilter] = React.useState<OrderStatus | "all">(
|
|
|
|
|
|
"all",
|
|
|
|
|
|
);
|
|
|
|
|
|
const [currentPage, setCurrentPage] = React.useState(1);
|
|
|
|
|
|
const [isHydrated, setIsHydrated] = React.useState(false);
|
|
|
|
|
|
const pageSize = 6;
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
const nextStatus = params.get("status");
|
|
|
|
|
|
if (
|
|
|
|
|
|
nextStatus &&
|
|
|
|
|
|
(nextStatus === "all" ||
|
|
|
|
|
|
orderStatuses.includes(nextStatus as OrderStatus))
|
|
|
|
|
|
) {
|
|
|
|
|
|
setStatusFilter(nextStatus as OrderStatus | "all");
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextPage = Number(params.get("page"));
|
|
|
|
|
|
if (!Number.isNaN(nextPage) && nextPage > 0) {
|
|
|
|
|
|
setCurrentPage(nextPage);
|
|
|
|
|
|
}
|
|
|
|
|
|
setIsHydrated(true);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const filteredOrders = React.useMemo(() => {
|
|
|
|
|
|
if (statusFilter === "all") return orders;
|
|
|
|
|
|
return orders.filter((order) => order.status === statusFilter);
|
|
|
|
|
|
}, [statusFilter]);
|
|
|
|
|
|
|
|
|
|
|
|
const totalPages = Math.max(1, Math.ceil(filteredOrders.length / pageSize));
|
|
|
|
|
|
|
|
|
|
|
|
const paginatedOrders = React.useMemo(() => {
|
|
|
|
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
|
|
|
|
return filteredOrders.slice(startIndex, startIndex + pageSize);
|
|
|
|
|
|
}, [filteredOrders, currentPage, pageSize]);
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
setCurrentPage(1);
|
|
|
|
|
|
}, [statusFilter]);
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
if (!isHydrated || typeof window === "undefined") return;
|
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
if (statusFilter !== "all") {
|
|
|
|
|
|
params.set("status", statusFilter);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params.delete("status");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentPage > 1) {
|
|
|
|
|
|
params.set("page", String(currentPage));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
params.delete("page");
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextQuery = params.toString();
|
|
|
|
|
|
const nextUrl = nextQuery
|
|
|
|
|
|
? `${window.location.pathname}?${nextQuery}`
|
|
|
|
|
|
: window.location.pathname;
|
|
|
|
|
|
window.history.replaceState(null, "", nextUrl);
|
|
|
|
|
|
}, [statusFilter, currentPage, isHydrated]);
|
|
|
|
|
|
|
|
|
|
|
|
const goToPage = (page: number) => {
|
|
|
|
|
|
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startRow = filteredOrders.length ? (currentPage - 1) * pageSize + 1 : 0;
|
|
|
|
|
|
const endRow = Math.min(currentPage * pageSize, filteredOrders.length);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={cn("rounded-xl border bg-card", className)}>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3 px-4 pt-4 sm:px-6">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<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"
|
|
|
|
|
|
>
|
|
|
|
|
|
<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={tableHeadClassName}>شماره سفارش</TableHead>
|
|
|
|
|
|
<TableHead className={tableHeadClassName}>خریدار</TableHead>
|
|
|
|
|
|
<TableHead className={tableHeadClassName}>مجموع</TableHead>
|
|
|
|
|
|
<TableHead className={tableHeadClassName}>وضعیت</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 RecentActivity = ({ className }: { className?: string }) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={cn("rounded-xl border bg-card", className)}>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3 px-4 pt-4 sm:px-6">
|
|
|
|
|
|
<h2 className="text-sm font-medium text-pretty sm:text-base">
|
2026-07-15 20:22:14 +02:00
|
|
|
|
فعالیت تیمهای اخیر
|
2026-07-11 21:21:29 +03:30
|
|
|
|
</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<ScrollArea className="h-[360px] px-4 pt-2 pb-4 text-xs sm:px-6 sm:text-sm">
|
|
|
|
|
|
<div className="divide-y">
|
|
|
|
|
|
{recentActivity.map((item) => (
|
|
|
|
|
|
<div key={`${item.title}-${item.minutesAgo}`} className="py-3">
|
|
|
|
|
|
<div className="flex min-w-0 items-center justify-between gap-3">
|
|
|
|
|
|
<span className="min-w-0 truncate font-medium text-foreground">
|
|
|
|
|
|
{item.title}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="shrink-0 text-[10px] text-muted-foreground sm:text-xs">
|
|
|
|
|
|
{formatRelativeTime(item.minutesAgo)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-[10px] break-words text-muted-foreground sm:text-xs">
|
|
|
|
|
|
{item.detail}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const DashboardContent = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
id="dashboard-main"
|
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
|
className="w-full flex-1 space-y-4 overflow-auto bg-background p-3 sm:p-4 md:p-6"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex flex-col gap-4 xl:flex-row">
|
|
|
|
|
|
<RevenueFlowChart />
|
|
|
|
|
|
<StatsCardsVertical />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col gap-4 xl:flex-row">
|
|
|
|
|
|
<RecentTransactionsTable className="flex-1" />
|
|
|
|
|
|
<RecentActivity className="xl:w-[360px]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const Dashboard3 = ({ 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 { Dashboard3 };
|