"use client"; 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, LibraryBig, LineChart as LineChartIcon, MessageCircle, Save, Tags, UsersRound, WalletCards, } from "lucide-react"; import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Scatter, ScatterChart, XAxis, YAxis, ZAxis, } from "recharts"; 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 { Label } from "@/components/ui/label"; 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 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}

); } function SectionError({ error }: { error: unknown }) { return ( {resolveErrorMessage(error)} ); } function SectionLoading() { return ( در حال بارگذاری داده‌های داشبورد... ); } function EmptyChart({ label = "داده‌ای برای نمایش وجود ندارد." }: { label?: string }) { return (
{label}
); } function DateRangeFilter({ value, onChange, onReset, }: { value: DateRangeState; onChange: (next: DateRangeState) => void; onReset: () => void; }) { return (
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" />
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" />
); } function FilterCard({ title, description, children, }: { title: string; 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 ? ( ) : ( <> formatNumberPersian(Number(value))} /> truncateLabel(String(value), 14)} /> } /> {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 ( رویدادهای برتر بر اساس شرکت‌کننده تاییدشده، درآمد و زمان برگزاری {group.top_items.length ? ( group.top_items.map((event, index) => (

{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)}
)) ) : (

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

)}
); } 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 (
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() { return (

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

گزارش‌های جداگانه برای کاربران، رویدادها و بلاگ با فیلترهای مستقل و خوانا

کاربران رویدادها بلاگ
); }