diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx new file mode 100644 index 0000000..6a8b557 --- /dev/null +++ b/src/views/AdminDashboard.tsx @@ -0,0 +1,592 @@ +"use client"; + +import * as React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + Activity, + BarChart3, + BookOpen, + CalendarDays, + Heart, + Landmark, + LineChart as LineChartIcon, + Save, + TrendingUp, + UsersRound, + WalletCards, +} from "lucide-react"; +import { + Bar, + BarChart, + CartesianGrid, + Cell, + Line, + LineChart, + Scatter, + ScatterChart, + XAxis, + YAxis, + ZAxis, +} from "recharts"; +import { api } from "@/lib/api"; +import type { + AdminDashboardAnalyticsSchema, + AnalyticsPointSchema, + AnalyticsPostPopularitySchema, + AnalyticsTrendPointSchema, + EventListItemSchema, +} from "@/lib/types"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { formatNumberPersian, formatToman, resolveErrorMessage, toPersianDigits } from "@/lib/utils"; + +const chartColors = [ + "hsl(var(--primary))", + "hsl(var(--chart-2, 173 58% 39%))", + "hsl(var(--chart-3, 197 37% 24%))", + "hsl(var(--chart-4, 43 74% 66%))", + "hsl(var(--chart-5, 27 87% 67%))", +]; + +type DashboardFilters = { + date_from: string; + date_to: string; + event_id: string; + granularity: "auto" | "day" | "week" | "month"; +}; + +function StatCard({ + title, + value, + description, + icon: Icon, +}: { + title: string; + value: string; + description: string; + icon: React.ComponentType<{ className?: string }>; +}) { + return ( + + +
+

{title}

+

{value}

+

{description}

+
+
+ +
+
+
+ ); +} + +function EmptyChart({ label = "داده‌ای برای نمایش وجود ندارد." }: { label?: string }) { + return ( +
+ {label} +
+ ); +} + +function VerticalBarChart({ data, color = "var(--color-value)" }: { data: AnalyticsPointSchema[]; color?: string }) { + if (!data.length) return ; + return ( + + + + formatNumberPersian(Number(value))} /> + + } /> + + + + ); +} + +function TrendLineChart({ data, color = "var(--color-value)" }: { data: AnalyticsTrendPointSchema[]; color?: string }) { + if (!data.length) return ; + return ( + + + + + formatNumberPersian(Number(value))} /> + } /> + + + + ); +} + +function StatusBarChart({ data }: { data: AdminDashboardAnalyticsSchema["events"]["registration_status"] }) { + if (!data.length) return ; + return ( + + + + + formatNumberPersian(Number(value))} /> + } /> + + {data.map((_, index) => ( + + ))} + + + + ); +} + +function BlogScatterChart({ data }: { data: AnalyticsPostPopularitySchema[] }) { + if (!data.length) return ; + return ( + + + + formatNumberPersian(Number(value))} + /> + formatNumberPersian(Number(value))} + /> + + { + if (!active || !payload?.length) return null; + const item = payload[0].payload as AnalyticsPostPopularitySchema; + return ( +
+

{item.title}

+
+

لایک: {formatNumberPersian(item.likes)}

+

ذخیره: {formatNumberPersian(item.saves)}

+

کامنت: {formatNumberPersian(item.comments)}

+
+
+ ); + }} + /> + +
+
+ ); +} + +function TopList({ + title, + items, + renderMeta, +}: { + title: string; + items: Array<{ id: number; title: string }>; + renderMeta: (item: { id: number; title: string }) => React.ReactNode; +}) { + return ( + + + {title} + + + {items.length ? ( + items.map((item, index) => ( +
+
+

{item.title}

+
{renderMeta(item)}
+
+ {toPersianDigits(index + 1)} +
+ )) + ) : ( +

داده‌ای وجود ندارد.

+ )} +
+
+ ); +} + +function ActivityTrendChart({ data }: { data: AdminDashboardAnalyticsSchema["blog"]["activity_trend"] }) { + if (!data.length) return ; + return ( + + + + + formatNumberPersian(Number(value))} /> + } /> + + + + + + ); +} + +export default function AdminDashboard() { + const [filters, setFilters] = React.useState({ + date_from: "", + date_to: "", + event_id: "all", + granularity: "auto", + }); + + const eventsQuery = useQuery({ + queryKey: ["admin", "dashboard", "events"], + queryFn: () => api.getEvents({ limit: 100 }), + }); + + const dashboardQuery = useQuery({ + queryKey: ["admin", "dashboard", filters], + queryFn: () => + api.getAdminDashboard({ + date_from: filters.date_from || undefined, + date_to: filters.date_to || undefined, + event_id: filters.event_id === "all" ? undefined : Number(filters.event_id), + granularity: filters.granularity, + }), + }); + + const dashboard = dashboardQuery.data; + const selectedEvent = React.useMemo(() => { + if (filters.event_id === "all") return null; + return (eventsQuery.data ?? []).find((event) => String(event.id) === filters.event_id) ?? null; + }, [eventsQuery.data, filters.event_id]); + + const setFilter = (key: K, value: DashboardFilters[K]) => { + setFilters((current) => ({ ...current, [key]: value })); + }; + + const resetFilters = () => { + setFilters({ date_from: "", date_to: "", event_id: "all", granularity: "auto" }); + }; + + return ( +
+
+
+

داشبورد دستاوردها

+

+ نمای کلی از رشد کاربران، تنوع رویدادها، درآمد و تعاملات بلاگ انجمن +

+
+ {selectedEvent ? فیلتر رویداد: {selectedEvent.title} : null} +
+ + + + + + فیلتر گزارش + + بازه زمانی روی هر بخش با تاریخ مناسب همان بخش اعمال می‌شود. + + +
+
+ + setFilter("date_from", event.target.value)} + /> +
+
+ + setFilter("date_to", event.target.value)} + /> +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + {dashboardQuery.isLoading ? ( + + در حال بارگذاری داده‌های داشبورد... + + ) : dashboardQuery.isError ? ( + + + {resolveErrorMessage(dashboardQuery.error)} + + + ) : dashboard ? ( + + ) : null} +
+ ); +} + +function DashboardContent({ dashboard }: { dashboard: AdminDashboardAnalyticsSchema }) { + return ( + <> +
+ + + + +
+ +
+ + + + +
+ +
+ + + ترکیب کاربران بر اساس رشته + کاربران ثبت‌نام‌شده در بازه انتخابی + + + + + + + + ترکیب کاربران بر اساس دانشگاه + برای سنجش گستره جذب انجمن + + + + + +
+ +
+ + + + + روند درآمد موفق + + فقط پرداخت‌های تاییدشده درگاه + + + + + + + + وضعیت ثبت‌نام‌ها + همه وضعیت‌ها در بازه/رویداد انتخابی + + + + + +
+ +
+ + + تنوع رشته در رویدادها + ثبت‌نام‌های تاییدشده و حاضرشده + + + + + + + + تنوع دانشگاه در رویدادها + قابل فیلتر برای هر رویداد + + + + + +
+ +
+ + + محبوبیت نوشته‌ها + نمودار نقطه‌ای لایک در برابر ذخیره؛ اندازه نقطه بر اساس کامنت + + + + + + + + روند تعاملات بلاگ + لایک، ذخیره و کامنت در زمان + + + + + +
+ +
+ { + const event = dashboard.events.top_events.find((candidate) => candidate.id === item.id); + if (!event) return null; + return ( + + {formatNumberPersian(event.attendees)} نفر + {event.fill_rate != null ? ` · ${formatNumberPersian(event.fill_rate)}٪ ظرفیت` : ""} + {event.revenue ? ` · ${formatToman(event.revenue)}` : ""} + + ); + }} + /> + { + const post = dashboard.blog.top_posts.find((candidate) => candidate.id === item.id); + if (!post) return null; + return ( + + {formatNumberPersian(post.likes)} لایک · {formatNumberPersian(post.saves)} ذخیره ·{" "} + {formatNumberPersian(post.comments)} کامنت + + ); + }} + /> + + + موضوعات فعال بلاگ + دسته‌بندی‌ها و تگ‌های دارای نوشته منتشرشده + + +
+

دسته‌بندی‌ها

+ +
+
+

تگ‌ها

+ +
+
+
+
+ + + + + + روند رشد کاربران و ثبت‌نام رویدادها + + + + + + + + + ); +}