diff --git a/src/lib/api.ts b/src/lib/api.ts index 843915d..eab9c99 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -432,6 +432,44 @@ class ApiClient { ); } + async getAdminUserAnalytics(params?: { date_from?: string; date_to?: string }) { + const query = new URLSearchParams(); + if (params?.date_from) query.set('date_from', params.date_from); + if (params?.date_to) query.set('date_to', params.date_to); + return this.request( + `/api/analytics/admin/users${query.toString() ? `?${query.toString()}` : ''}`, + ); + } + + async getAdminEventAnalytics(params?: { date_from?: string; date_to?: string; event_id?: number }) { + const query = new URLSearchParams(); + if (params?.date_from) query.set('date_from', params.date_from); + if (params?.date_to) query.set('date_to', params.date_to); + if (params?.event_id != null) query.set('event_id', String(params.event_id)); + return this.request( + `/api/analytics/admin/events${query.toString() ? `?${query.toString()}` : ''}`, + ); + } + + async getAdminBlogAnalytics(params?: { date_from?: string; date_to?: string }) { + const query = new URLSearchParams(); + if (params?.date_from) query.set('date_from', params.date_from); + if (params?.date_to) query.set('date_to', params.date_to); + return this.request( + `/api/analytics/admin/blog${query.toString() ? `?${query.toString()}` : ''}`, + ); + } + + async getAdminDashboardEventOptions(params?: { search?: string; limit?: number; offset?: number }) { + const query = new URLSearchParams(); + if (params?.search) query.set('search', params.search); + if (params?.limit != null) query.set('limit', String(params.limit)); + if (params?.offset != null) query.set('offset', String(params.offset)); + return this.request( + `/api/analytics/admin/events/options${query.toString() ? `?${query.toString()}` : ''}`, + ); + } + // ============= Blog Endpoints ============= async getPosts(params?: { diff --git a/src/lib/types.ts b/src/lib/types.ts index c3bb4ac..ad107ee 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -759,6 +759,12 @@ export interface AnalyticsPointSchema { value: number; } +export interface AnalyticsPointGroupSchema { + top_items: AnalyticsPointSchema[]; + other_count: number; + total_count: number; +} + export interface AnalyticsTrendPointSchema { date: string; label: string; @@ -790,6 +796,12 @@ export interface AnalyticsPostPopularitySchema { comments: number; } +export interface AnalyticsPostPopularityGroupSchema { + top_items: AnalyticsPostPopularitySchema[]; + other_count: number; + total_count: number; +} + export interface AnalyticsTopPostSchema extends AnalyticsPostPopularitySchema { score: number; } @@ -855,6 +867,81 @@ export interface AdminDashboardAnalyticsSchema { }; } +export interface AnalyticsEventOptionsSchema { + count: number; + results: Array<{ + value: string; + label: string; + description?: string | null; + }>; +} + +export interface UserAnalyticsSchema { + filters: { + date_from?: string | null; + date_to?: string | null; + granularity: 'day' | 'week' | 'month'; + }; + summary: { + total_users: number; + verified_users: number; + unverified_users: number; + profile_completion_rate: number; + }; + signup_trend: AnalyticsTrendPointSchema[]; + by_major: AnalyticsPointGroupSchema; + by_university: AnalyticsPointGroupSchema; + by_year: AnalyticsPointGroupSchema; +} + +export interface EventAnalyticsSchema { + filters: { + date_from?: string | null; + date_to?: string | null; + event_id?: number | null; + }; + summary: { + total_events: number; + total_registrations: number; + distinct_participants: number; + total_revenue: number; + total_discount: number; + total_base: number; + learning_hours: number; + }; + registration_status: AnalyticsRegistrationStatusSchema[]; + payment_status: AnalyticsRegistrationStatusSchema[]; + attendee_by_major: AnalyticsPointGroupSchema; + attendee_by_university: AnalyticsPointGroupSchema; + registration_trend: AnalyticsTrendPointSchema[]; + revenue_trend: AnalyticsTrendPointSchema[]; + revenue_by_event: AnalyticsPointGroupSchema; + top_events: { + top_items: AnalyticsTopEventSchema[]; + other_count: number; + total_count: number; + }; +} + +export interface BlogAnalyticsSchema { + filters: { + date_from?: string | null; + date_to?: string | null; + }; + summary: { + published_posts: number; + total_likes: number; + total_saves: number; + total_comments: number; + community_engagement: number; + }; + activity_trend: Array<{ date: string; likes: number; saves: number; comments: number }>; + post_popularity: AnalyticsPostPopularityGroupSchema; + top_posts: AnalyticsTopPostSchema[]; + by_category: AnalyticsPointGroupSchema; + by_tag: AnalyticsPointGroupSchema; +} + // payment export interface CreatePaymentOut { start_pay_url: string; diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx index 6a8b557..aab9093 100644 --- a/src/views/AdminDashboard.tsx +++ b/src/views/AdminDashboard.tsx @@ -2,16 +2,22 @@ import * as React from "react"; import { useQuery } from "@tanstack/react-query"; +import DateObject from "react-date-object"; +import persian from "react-date-object/calendars/persian"; +import persian_fa from "react-date-object/locales/persian_fa"; +import DatePicker from "react-multi-date-picker"; import { Activity, BarChart3, BookOpen, CalendarDays, + GraduationCap, Heart, - Landmark, + LibraryBig, LineChart as LineChartIcon, + MessageCircle, Save, - TrendingUp, + Tags, UsersRound, WalletCards, } from "lucide-react"; @@ -28,58 +34,124 @@ import { YAxis, ZAxis, } from "recharts"; -import { api } from "@/lib/api"; -import type { - AdminDashboardAnalyticsSchema, - AnalyticsPointSchema, - AnalyticsPostPopularitySchema, - AnalyticsTrendPointSchema, - EventListItemSchema, -} from "@/lib/types"; +import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox"; 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"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { api } from "@/lib/api"; +import type { + AnalyticsPointGroupSchema, + AnalyticsPointSchema, + AnalyticsPostPopularitySchema, + AnalyticsTrendPointSchema, + BlogAnalyticsSchema, + EventAnalyticsSchema, + UserAnalyticsSchema, +} from "@/lib/types"; +import { + cn, + formatJalaliDate, + 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"; +const PALETTE = { + teal: "#2dd4bf", + cyan: "#38bdf8", + amber: "#fbbf24", + rose: "#fb7185", + violet: "#a78bfa", + emerald: "#34d399", + slate: "#94a3b8", }; +const STATUS_COLORS = [PALETTE.teal, PALETTE.cyan, PALETTE.amber, PALETTE.rose, PALETTE.violet, PALETTE.emerald]; + +type DateRangeState = { + from: string; + to: string; +}; + +type SectionState = DateRangeState & { + eventId?: string | null; +}; + +function toApiDate(date: DateObject | null) { + if (!date) return ""; + const gregorian = date.toDate(); + const year = gregorian.getFullYear(); + const month = String(gregorian.getMonth() + 1).padStart(2, "0"); + const day = String(gregorian.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +} + +function fromApiDate(value?: string | null) { + if (!value) return null; + const [year, month, day] = value.split("-").map(Number); + if (!year || !month || !day) return null; + return new DateObject({ + date: new Date(year, month - 1, day), + calendar: persian, + locale: persian_fa, + }); +} + +function formatJalaliTick(value?: string | number) { + if (!value) return ""; + const raw = String(value); + const date = new Date(raw.length === 10 ? `${raw}T00:00:00` : raw); + if (Number.isNaN(date.getTime())) return toPersianDigits(raw); + const locale = Intl.DateTimeFormat.supportedLocalesOf(["fa-IR-u-ca-persian"]).length + ? "fa-IR-u-ca-persian" + : "fa-IR"; + return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(date); +} + +function truncateLabel(value: string, max = 18) { + const normalized = toPersianDigits(value); + return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized; +} + +function chartHeight(count: number, min = 280) { + return Math.max(min, count * 38 + 90); +} + +function axisWidth(items: AnalyticsPointSchema[]) { + const maxLength = Math.max(...items.map((item) => String(item.label).length), 10); + return Math.min(190, Math.max(90, maxLength * 7)); +} + +function dataWithOtherNotice(group: AnalyticsPointGroupSchema) { + return group.top_items; +} + function StatCard({ title, value, description, icon: Icon, + tone = "teal", }: { title: string; value: string; description: string; icon: React.ComponentType<{ className?: string }>; + tone?: keyof typeof PALETTE; }) { return (

{title}

-

{value}

-

{description}

+

{value}

+

{description}

-
+
@@ -87,6 +159,22 @@ function StatCard({ ); } +function SectionError({ error }: { error: unknown }) { + return ( + + {resolveErrorMessage(error)} + + ); +} + +function SectionLoading() { + return ( + + در حال بارگذاری داده‌های داشبورد... + + ); +} + function EmptyChart({ label = "داده‌ای برای نمایش وجود ندارد." }: { label?: string }) { return (
@@ -95,124 +183,466 @@ function EmptyChart({ label = "داده‌ای برای نمایش وجود ند ); } -function VerticalBarChart({ data, color = "var(--color-value)" }: { data: AnalyticsPointSchema[]; color?: string }) { - if (!data.length) return ; +function DateRangeFilter({ + value, + onChange, + onReset, +}: { + value: DateRangeState; + onChange: (next: DateRangeState) => void; + onReset: () => void; +}) { 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))} +
+
+ + onChange({ ...value, from: toApiDate(next instanceof DateObject ? next : null) })} + calendar={persian} + locale={persian_fa} + calendarPosition="bottom-right" + inputClass="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-right ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + placeholder="تاریخ شروع" + containerClassName="w-full" /> - formatNumberPersian(Number(value))} +
+
+ + onChange({ ...value, to: toApiDate(next instanceof DateObject ? next : null) })} + calendar={persian} + locale={persian_fa} + calendarPosition="bottom-right" + inputClass="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-right ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + placeholder="تاریخ پایان" + containerClassName="w-full" /> - - { - 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({ +function FilterCard({ title, - items, - renderMeta, + description, + children, }: { title: string; - items: Array<{ id: number; title: string }>; - renderMeta: (item: { id: number; title: string }) => React.ReactNode; + description: string; + children: React.ReactNode; +}) { + return ( + + + + + {title} + + {description} + + {children} + + ); +} + +function ChartViewport({ children, minWidth = 560 }: { children: React.ReactNode; minWidth?: number }) { + return ( +
+
{children}
+
+ ); +} + +function CustomValueTooltip({ + active, + payload, + unit, + formatter, +}: { + active?: boolean; + payload?: Array<{ payload?: AnalyticsPointSchema; value?: number }>; + unit?: string; + formatter?: (value: number) => string; +}) { + if (!active || !payload?.length || !payload[0].payload) return null; + const item = payload[0].payload; + return ( +
+

{toPersianDigits(item.label)}

+

+ {formatter ? formatter(Number(item.value)) : formatNumberPersian(item.value)} + {unit ? ` ${unit}` : ""} +

+
+ ); +} + +function HighCardinalityNotice({ group }: { group: AnalyticsPointGroupSchema }) { + if (group.total_count <= group.top_items.length) return null; + return ( +

+ نمایش {formatNumberPersian(group.top_items.length)} مورد برتر از {formatNumberPersian(group.total_count)} مورد؛{" "} + {formatNumberPersian(group.other_count)} مورد دیگر در نمودار فشرده نشده‌اند. +

+ ); +} + +function ValuesTable({ + data, + unit, + formatter, +}: { + data: AnalyticsPointSchema[]; + unit?: string; + formatter?: (value: number) => string; +}) { + if (!data.length) return null; + return ( +
+ + + {data.map((item, index) => ( + + + + + + ))} + +
{toPersianDigits(index + 1)}{toPersianDigits(item.label)} + {formatter ? formatter(Number(item.value)) : formatNumberPersian(item.value)} + {unit ? ` ${unit}` : ""} +
+
+ ); +} + +function HorizontalBarCard({ + title, + description, + group, + color = PALETTE.teal, + unit, + valueFormatter, +}: { + title: string; + description: string; + group: AnalyticsPointGroupSchema; + color?: string; + unit?: string; + valueFormatter?: (value: number) => string; +}) { + const data = dataWithOtherNotice(group); + return ( + + + {title} + {description} + + + {!data.length ? ( + + ) : ( + <> + + + + + valueFormatter ? valueFormatter(Number(value)) : formatNumberPersian(Number(value))} /> + truncateLabel(String(value))} + /> + } /> + + + + + + + + )} + + + ); +} + +function TrendLineCard({ + title, + description, + data, + color = PALETTE.teal, + valueFormatter = formatNumberPersian, +}: { + title: string; + description: string; + data: AnalyticsTrendPointSchema[]; + color?: string; + valueFormatter?: (value: number) => string; +}) { + return ( + + + + + {title} + + {description} + + + {!data.length ? ( + + ) : ( + + + + + + valueFormatter(Number(value))} /> + { + if (!active || !payload?.length) return null; + const item = payload[0].payload as AnalyticsTrendPointSchema; + return ( +
+

{formatJalaliDate(item.date)}

+

{valueFormatter(Number(item.value))}

+
+ ); + }} + /> + +
+
+
+ )} +
+
+ ); +} + +function StatusChartCard({ + title, + description, + data, +}: { + title: string; + description: string; + data: Array<{ status: string; label: string; value: number }>; }) { return ( {title} + {description} + + + {!data.length ? ( + + ) : ( + <> + + + + + truncateLabel(String(value), 14)} /> + formatNumberPersian(Number(value))} /> + } /> + + {data.map((_, index) => ( + + ))} + + + + + ({ label: item.label, value: item.value }))} /> + + )} + + + ); +} + +function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend"] }) { + return ( + + + روند تعاملات بلاگ + لایک، ذخیره و کامنت در بازه انتخابی + + + {!data.length ? ( + + ) : ( + + + + + + formatNumberPersian(Number(value))} /> + } /> + + + + + + + )} + + + ); +} + +function BlogScatterCard({ group }: { group: BlogAnalyticsSchema["post_popularity"] }) { + const data = group.top_items; + return ( + + + محبوبیت نوشته‌ها + لایک در برابر ذخیره؛ اندازه نقطه بر اساس تعداد کامنت + + + {!data.length ? ( + + ) : ( + <> + + + + + 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)}

+
+
+ ); + }} + /> + +
+
+
+ {group.total_count > data.length ? ( +

+ نمایش {formatNumberPersian(data.length)} نوشته برتر از {formatNumberPersian(group.total_count)} نوشته دارای تعامل. +

+ ) : null} + + )} +
+
+ ); +} + +function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] }) { + return ( + + + رویدادهای برتر + بر اساس شرکت‌کننده تاییدشده، درآمد و زمان برگزاری - {items.length ? ( - items.map((item, index) => ( -
+ {group.top_items.length ? ( + group.top_items.map((event, index) => ( +
-

{item.title}

-
{renderMeta(item)}
+

{event.title}

+

+ {formatNumberPersian(event.attendees)} نفر + {event.fill_rate != null ? ` · ${formatNumberPersian(event.fill_rate)}٪ ظرفیت` : ""} + {event.revenue ? ` · ${formatToman(event.revenue)}` : ""} +

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

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

+ )} + {group.total_count > group.top_items.length ? ( +

+ {formatNumberPersian(group.other_count)} رویداد دیگر در این لیست خلاصه نشده‌اند. +

+ ) : null} + + + ); +} + +function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) { + return ( + + + نوشته‌های برتر + بر اساس جمع لایک، ذخیره و کامنت + + + {posts.length ? ( + posts.map((post, index) => ( +
+
+

{post.title}

+

+ {formatNumberPersian(post.likes)} لایک · {formatNumberPersian(post.saves)} ذخیره ·{" "} + {formatNumberPersian(post.comments)} کامنت +

{toPersianDigits(index + 1)}
@@ -225,368 +655,230 @@ function TopList({ ); } -function ActivityTrendChart({ data }: { data: AdminDashboardAnalyticsSchema["blog"]["activity_trend"] }) { - if (!data.length) return ; +function UsersSection() { + const [filters, setFilters] = React.useState({ from: "", to: "" }); + const query = useQuery({ + queryKey: ["admin", "analytics", "users", filters], + queryFn: () => api.getAdminUserAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }), + }); + return ( - - - - - formatNumberPersian(Number(value))} /> - } /> - - - - - +
+ + setFilters({ from: "", to: "" })} /> + + {query.isLoading ? : null} + {query.isError ? : null} + {query.data ? : null} +
+ ); +} + +function UsersContent({ data }: { data: UserAnalyticsSchema }) { + return ( + <> +
+ + + + +
+
+ + +
+
+ + +
+ + ); +} + +function EventsSection() { + const [filters, setFilters] = React.useState({ from: "", to: "", eventId: null }); + const query = useQuery({ + queryKey: ["admin", "analytics", "events", filters], + queryFn: () => + api.getAdminEventAnalytics({ + date_from: filters.from || undefined, + date_to: filters.to || undefined, + event_id: filters.eventId ? Number(filters.eventId) : undefined, + }), + }); + + const reset = () => setFilters({ from: "", to: "", eventId: null }); + + return ( +
+ +
+
+ setFilters((current) => ({ ...current, ...next }))} + onReset={reset} + /> +
+
+ + setFilters((current) => ({ ...current, eventId }))} + loadOptions={async ({ search, limit, offset }) => { + const data = await api.getAdminDashboardEventOptions({ search, limit, offset }); + return { + count: data.count, + results: data.results.map((item) => ({ + ...item, + description: item.description ?? undefined, + })), + }; + }} + placeholder="همه رویدادها" + searchPlaceholder="جستجوی رویداد..." + emptyText="رویدادی پیدا نشد." + /> +
+
+ +
+
+
+ {query.isLoading ? : null} + {query.isError ? : null} + {query.data ? : null} +
+ ); +} + +function EventsContent({ data }: { data: EventAnalyticsSchema }) { + return ( + <> +
+ + + + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+ + ); +} + +function BlogSection() { + const [filters, setFilters] = React.useState({ from: "", to: "" }); + const query = useQuery({ + queryKey: ["admin", "analytics", "blog", filters], + queryFn: () => api.getAdminBlogAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }), + }); + + return ( +
+ + setFilters({ from: "", to: "" })} /> + + {query.isLoading ? : null} + {query.isError ? : null} + {query.data ? : null} +
+ ); +} + +function BlogContent({ data }: { data: BlogAnalyticsSchema }) { + return ( + <> +
+ + + + +
+
+
+ +
+ +
+
+ + + +
+ ); } 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)} کامنت - - ); - }} - /> - - - موضوعات فعال بلاگ - دسته‌بندی‌ها و تگ‌های دارای نوشته منتشرشده - - -
-

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

- -
-
-

تگ‌ها

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