"use client"; import { useEffect, useState } from "react"; import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Award, Flame, Heart, Loader2, Lock, Search, Sparkles, Trophy, Unlock, Users, } from "lucide-react"; import { http, toUserError } from "@/lib/api"; import type { LearningOverview, StudentLearningRow, UserLearningDetail, } from "@/lib/types"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { toast } from "@/components/providers"; import { cn, formatFaNumber } from "@/lib/utils"; import { DEFAULT_PAGE_SIZE, ListPagination, buildPageQuery, type Paginated, } from "@/components/admin/list-pagination"; type SortKey = "xp" | "progress" | "activity" | "streak"; type FilterKey = "all" | "active" | "locked"; function pct(n: number) { return `${formatFaNumber(Math.round(n))}٪`; } function fmtDate(v?: string) { if (!v) return "—"; try { return new Date(v).toLocaleString("fa-IR", { dateStyle: "medium", timeStyle: "short" }); } catch { return v; } } export function LearningPanel() { const qc = useQueryClient(); const [q, setQ] = useState(""); const [filter, setFilter] = useState("all"); const [sort, setSort] = useState("xp"); const [selectedId, setSelectedId] = useState(null); const [unlocking, setUnlocking] = useState(false); const [issuingCertFor, setIssuingCertFor] = useState(null); const [page, setPage] = useState(1); useEffect(() => { setPage(1); }, [q, filter, sort]); const overviewQ = useQuery({ queryKey: ["admin", "learning", "overview"], queryFn: () => http.get<{ overview: LearningOverview }>("/api/admin/learning/overview"), retry: 1, }); const studentsQ = useQuery({ queryKey: ["admin", "learning", "students", q, filter, sort, page], queryFn: () => { const params = new URLSearchParams(buildPageQuery(page)); if (q.trim()) params.set("q", q.trim()); if (filter !== "all") params.set("filter", filter); params.set("sort", sort); return http.get>( `/api/admin/learning/students?${params.toString()}`, ); }, placeholderData: keepPreviousData, retry: 1, }); const detailQ = useQuery({ queryKey: ["admin", "learning", "user", selectedId], queryFn: () => http.get<{ learning: UserLearningDetail }>(`/api/admin/users/${selectedId}/learning`), enabled: selectedId != null, retry: 1, }); const unlockStudent = async (id: number) => { setUnlocking(true); try { await http.post(`/api/admin/users/${id}/unlock`); toast.success("قفل حساب باز شد و جان‌ها بازگردانی شدند"); await Promise.all([ qc.invalidateQueries({ queryKey: ["admin", "learning"] }), qc.invalidateQueries({ queryKey: ["admin", "learning", "user", id] }), ]); } catch (e) { toast.error(toUserError(e, "باز کردن قفل ممکن نشد")); } finally { setUnlocking(false); } }; const issueCertificate = async (id: number) => { setIssuingCertFor(id); try { await http.post(`/api/admin/users/${id}/certificate/issue`, {}); toast.success("گواهینامه با موفقیت صادر شد"); await qc.invalidateQueries({ queryKey: ["admin", "learning", "user", id] }); } catch (e) { toast.error(toUserError(e, "صدور گواهینامه ممکن نشد")); } finally { setIssuingCertFor(null); } }; const overview = overviewQ.data?.overview; const students = studentsQ.data?.items ?? []; const studentsTotal = studentsQ.data?.total ?? 0; const studentsPageSize = studentsQ.data?.pageSize ?? DEFAULT_PAGE_SIZE; const cards = overview ? [ { label: "هنرجویان فعال", value: overview.studentsActive, hint: `از ${formatFaNumber(overview.studentsTotal)} نفر`, icon: Users }, { label: "قفل‌شده", value: overview.studentsLocked, hint: "نیاز به باز کردن قفل", icon: Lock }, { label: "تکمیل درس", value: overview.completions, hint: `${formatFaNumber(overview.lessonsTotal)} درس فعال`, icon: Trophy }, { label: "نرخ قبولی ۷روزه", value: Math.round(overview.passRate7d), hint: `${formatFaNumber(overview.passes7d)}/${formatFaNumber(overview.attempts7d)} تلاش`, icon: Sparkles, suffix: "٪" }, { label: "میانگین تماشا", value: Math.round(overview.avgWatchPct), hint: "درس‌های ناتمام", icon: Flame, suffix: "٪" }, { label: "امتیاز ۷روزه", value: overview.xpAwarded7d, hint: `${formatFaNumber(overview.studentsStreaking)} با استریک`, icon: Sparkles }, ] : []; return (
{(overviewQ.isError || studentsQ.isError) && (

{toUserError(overviewQ.error ?? studentsQ.error, "بارگذاری داشبورد یادگیری ممکن نشد")}

)}
{overviewQ.isLoading && [0, 1, 2, 3, 4, 5].map((i) => )} {cards.map((c) => (

{formatFaNumber(c.value)} {c.suffix ?? ""}

{c.label}

{c.hint}

))}
جدول هنرجویان

{formatFaNumber(studentsTotal)} نفر · برای جزئیات روی ردیف بزنید

setQ(e.target.value)} placeholder="جستجوی نام یا ایمیل…" className="h-10 ps-9" />
{studentsQ.isLoading && (
{[0, 1, 2, 3].map((i) => ( ))}
)}
{students.map((s) => ( setSelectedId(s.id)} > ))}
هنرجو پیشرفت امتیاز جان استریک ۷روزه آخرین فعالیت

{s.name} {s.isLocked && ( قفل )}

@{s.username}

{formatFaNumber(s.lessonsPassed)}/{formatFaNumber(s.lessonsTotal)} {pct(s.progressPct)}
{formatFaNumber(s.xp)} {formatFaNumber(s.hearts)} {formatFaNumber(s.streakCurrent)} {formatFaNumber(s.passes7d)}/{formatFaNumber(s.attempts7d)} {s.attempts7d > 0 && ( ({pct(s.passRate7d)}) )} {fmtDate(s.lastActivityAt)}
{students.length === 0 && !studentsQ.isLoading && (

هنرجویی یافت نشد.

)}
!o && setSelectedId(null)}> {detailQ.data?.learning.user.name ?? "جزئیات یادگیری"} {detailQ.isError ? toUserError(detailQ.error, "بارگذاری جزئیات ممکن نشد") : detailQ.data ? `@${detailQ.data.learning.user.username} · ${pct(detailQ.data.learning.user.progressPct)} مسیر` : "در حال بارگذاری…"}
{detailQ.isLoading && (
بارگذاری جزئیات
)} {detailQ.isError && (

{toUserError(detailQ.error)}

)} {detailQ.data && ( <> {detailQ.data.learning.user.isLocked && (

حساب قفل است

پس از باز کردن قفل، سه جان بازمی‌گردد و مسیر ادامه پیدا می‌کند.

)}
{[ { l: "امتیاز", v: formatFaNumber(detailQ.data.learning.user.xp) }, { l: "جان", v: formatFaNumber(detailQ.data.learning.user.hearts) }, { l: "استریک", v: formatFaNumber(detailQ.data.learning.user.streakCurrent) }, { l: "تکمیل", v: `${formatFaNumber(detailQ.data.learning.user.lessonsPassed)}/${formatFaNumber(detailQ.data.learning.user.lessonsTotal)}`, }, ].map((x) => (

{x.l}

{x.v}

))}
{detailQ.data.learning.user.progressPct >= 100 && (

دوره تکمیل شده — گواهینامه قابل صدور است

)}

دروس

{detailQ.data.learning.lessons.map((l) => (

L{l.lessonId} · {l.title}

{l.chapterTitle}

{pct(l.watchedPct)} {l.passedQuiz ? ( قبول ) : l.quizUnlocked ? ( آزمون باز ) : ( در حال تماشا )}
))}

تلاش‌های آزمون

{detailQ.data.learning.attempts.length === 0 ? (

هنوز تلاشی ثبت نشده.

) : (
{detailQ.data.learning.attempts.map((a) => (
{a.lessonTitle} {formatFaNumber(a.correctCount)}/{formatFaNumber(a.total)} · {pct(a.scorePct)} {a.status === "passed" ? "قبول" : "رد"} {fmtDate(a.createdAt)}
))}
)}

امتیازها

{detailQ.data.learning.xpEvents.map((x) => (
+{formatFaNumber(x.amount)} {x.source} {fmtDate(x.createdAt)}
))} {detailQ.data.learning.xpEvents.length === 0 && (

رویداد امتیازی نیست.

)}

نشست‌های تماشا

{detailQ.data.learning.sessions.map((s) => (

{s.lessonTitle}

آخرین ضربان: {fmtDate(s.lastHeartbeatAt)}

))} {detailQ.data.learning.sessions.length === 0 && (

نشست تماشایی نیست.

)}
)}
); }