"use client"; import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Plus, Trash2, Trophy } from "lucide-react"; import { http, toUserError } from "@/lib/api"; 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 { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { JalaliDateTimePicker, isValidIsoRange } from "@/components/ui/jalali-datetime-picker"; import { useConfirm } from "@/components/admin/use-confirm"; import { toast } from "@/components/providers"; import { formatFaNumber } from "@/lib/utils"; export type AdminChallenge = { id: number; title: string; description: string; targetXp: number; startsAt: string; endsAt: string; isActive: boolean; createdAt: string; }; export function ChallengesPanel() { const qc = useQueryClient(); const { confirm, dialog } = useConfirm(); const [modal, setModal] = useState(null); const { data, isLoading, isError, error, refetch } = useQuery({ queryKey: ["admin", "challenges"], queryFn: () => http.get<{ challenges: AdminChallenge[] }>("/api/admin/challenges"), }); const save = async (c: AdminChallenge) => { if (!isValidIsoRange(c.startsAt, c.endsAt)) { toast.error("زمان پایان باید پس از زمان شروع باشد"); return; } if (!c.targetXp || c.targetXp <= 0) { toast.error("هدف امتیاز باید بزرگ‌تر از صفر باشد"); return; } try { const { createdAt: _c, ...body } = c; if (c.id) await http.put(`/api/admin/challenges/${c.id}`, body); else await http.post("/api/admin/challenges", body); setModal(null); qc.invalidateQueries({ queryKey: ["admin", "challenges"] }); qc.invalidateQueries({ queryKey: ["challenges"] }); toast.success(c.id ? "چالش به‌روزرسانی شد" : "چالش ساخته شد"); } catch (e) { toast.error(toUserError(e)); } }; const del = async (c: AdminChallenge) => { if (!(await confirm("حذف چالش", `چالش «${c.title}» حذف شود؟`))) return; try { await http.del(`/api/admin/challenges/${c.id}`); qc.invalidateQueries({ queryKey: ["admin", "challenges"] }); toast.success("چالش حذف شد"); } catch (e) { toast.error(toUserError(e)); } }; const fmt = (iso: string) => new Intl.DateTimeFormat("fa-IR", { weekday: "short", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }).format(new Date(iso)); return ( <> {dialog}
چالش‌های امتیاز

چالش ماهانه با هدف امتیاز؛ پیشرفت هنرجو با تکمیل آزمون‌ها به‌روز می‌شود.

{isLoading && (
{[0, 1].map((i) => ( ))}
)} {isError && (

{toUserError(error)}

)}
{(data?.challenges ?? []).map((c) => (
{c.isActive ? "فعال" : "خاموش"} هدف {formatFaNumber(c.targetXp)} امتیاز

{c.title}

{c.description || "—"}

{fmt(c.startsAt)} تا {fmt(c.endsAt)}

))}
{!isLoading && !isError && (data?.challenges ?? []).length === 0 && (

هنوز چالشی نیست.

)}
setModal(null)} onSave={save} />
); } function ChallengeDialog({ challenge, onClose, onSave, }: { challenge: AdminChallenge | null; onClose: () => void; onSave: (c: AdminChallenge) => void; }) { const [draft, setDraft] = useState(challenge); useEffect(() => setDraft(challenge), [challenge]); const set = (patch: Partial) => setDraft((d) => (d ? { ...d, ...patch } : d)); const valid = !!draft?.title.trim() && !!draft?.startsAt && !!draft?.endsAt && (draft?.targetXp ?? 0) > 0 && isValidIsoRange(draft!.startsAt, draft!.endsAt); return ( !o && onClose()}> {draft?.id ? "ویرایش چالش" : "چالش جدید"} بازه را با تقویم جلالی تنظیم کنید؛ هنرجویان پیشرفت تا هدف امتیاز را می‌بینند. {draft && (
set({ title: e.target.value })} placeholder="مثلاً چالش ماه مهر" />