diff --git a/src/views/AdminEventDetail.tsx b/src/views/AdminEventDetail.tsx
index 48ca146..8de99cc 100644
--- a/src/views/AdminEventDetail.tsx
+++ b/src/views/AdminEventDetail.tsx
@@ -1,216 +1,277 @@
"use client";
-import * as React from 'react';
-import { useParams, Link, Navigate } from '@/lib/router';
-import { useQuery } from '@tanstack/react-query';
-import { api } from '@/lib/api';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Input } from '@/components/ui/input';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import { useToast } from '@/hooks/use-toast';
-import { formatJalali, formatToman, resolveErrorMessage, toPersianDigits } from '@/lib/utils';
-import { useAuth } from '@/contexts/AuthContext';
+import * as React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { CheckCircle2, Clock3, XCircle } from "lucide-react";
+import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox";
+import Markdown from "@/components/Markdown";
+import ProgressiveImage from "@/components/ProgressiveImage";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Link, Navigate, useParams } from "@/lib/router";
+import type { RegistrationAdminSchema } from "@/lib/types";
+import { api } from "@/lib/api";
+import { formatJalali, formatToman, getEventCardImageUrl, resolveErrorMessage, toPersianDigits } from "@/lib/utils";
+import { useAuth } from "@/contexts/AuthContext";
+import { useToast } from "@/hooks/use-toast";
-const registrationStatusOptions = [
- { value: 'confirmed', label: 'تایید شده' },
- { value: 'pending', label: 'در انتظار' },
- { value: 'cancelled', label: 'لغو شده' },
- { value: 'attended', label: 'حضور یافته' },
-] as const;
const REGISTRATIONS_PAGE_SIZE = 10;
+const statusOptions = [
+ { value: "all", label: "همه" },
+ { value: "confirmed", label: "تایید شده" },
+ { value: "pending", label: "در انتظار" },
+ { value: "cancelled", label: "لغو شده" },
+ { value: "attended", label: "حضور یافته" },
+] as const;
+
+function initials(registration: RegistrationAdminSchema) {
+ const text = [registration.user.first_name, registration.user.last_name].filter(Boolean).join(" ") || registration.user.username;
+ return text.slice(0, 2).toUpperCase();
+}
+
+function statusIcon(status: RegistrationAdminSchema["status"]) {
+ if (status === "confirmed" || status === "attended") return ;
+ if (status === "pending") return ;
+ return ;
+}
+
+function RegistrationDialog({
+ registration,
+ onOpenChange,
+}: {
+ registration: RegistrationAdminSchema | null;
+ onOpenChange: (open: boolean) => void;
+}) {
+ return (
+
+ );
+}
+
+function Info({ label, value }: { label: string; value?: React.ReactNode }) {
+ return (
+
+ {label}
+ {value || "—"}
+
+ );
+}
+
export default function AdminEventDetail() {
const { id } = useParams();
const { toast } = useToast();
const { user, isAuthenticated, loading } = useAuth();
- const [statusFilter, setStatusFilter] = React.useState('all');
- const [search, setSearch] = React.useState('');
- const [regPage, setRegPage] = React.useState(1);
-
const eventId = Number(id);
+ const [statusFilter, setStatusFilter] = React.useState("all");
+ const [search, setSearch] = React.useState("");
+ const [debouncedSearch, setDebouncedSearch] = React.useState("");
+ const [university, setUniversity] = React.useState(null);
+ const [major, setMajor] = React.useState(null);
+ const [regPage, setRegPage] = React.useState(1);
+ const [selectedRegistration, setSelectedRegistration] = React.useState(null);
+
+ React.useEffect(() => {
+ const timer = window.setTimeout(() => setDebouncedSearch(search.trim()), 300);
+ return () => window.clearTimeout(timer);
+ }, [search]);
+
const detailQuery = useQuery({
- queryKey: ['admin', 'event-detail', eventId],
+ queryKey: ["admin", "event-detail", eventId],
queryFn: () => api.getEventAdminDetail(eventId),
enabled: Number.isFinite(eventId),
});
const registrationsQuery = useQuery({
- queryKey: ['admin', 'event', eventId, 'registrations', statusFilter, search, regPage],
+ queryKey: ["admin", "event", eventId, "registrations", statusFilter, debouncedSearch, university, major, regPage],
enabled: Number.isFinite(eventId),
queryFn: () =>
api.listEventRegistrationsAdmin(eventId, {
- statuses:
- statusFilter === 'all'
- ? registrationStatusOptions.map((s) => s.value)
- : [statusFilter],
- search: search || undefined,
+ statuses: statusFilter === "all" ? undefined : [statusFilter],
+ search: debouncedSearch || undefined,
+ university: university || undefined,
+ major: major || undefined,
limit: REGISTRATIONS_PAGE_SIZE,
offset: (regPage - 1) * REGISTRATIONS_PAGE_SIZE,
}),
});
React.useEffect(() => {
- if (detailQuery.error) {
- toast({ title: 'خطا در دریافت جزئیات رویداد', description: resolveErrorMessage(detailQuery.error), variant: 'destructive' });
- }
- }, [detailQuery.error, toast]);
+ const error = detailQuery.error || registrationsQuery.error;
+ if (error) toast({ title: "خطا در دریافت اطلاعات رویداد", description: resolveErrorMessage(error), variant: "destructive" });
+ }, [detailQuery.error, registrationsQuery.error, toast]);
- React.useEffect(() => {
- if (registrationsQuery.error) {
- toast({ title: 'خطا در ثبتنامها', description: resolveErrorMessage(registrationsQuery.error), variant: 'destructive' });
- }
- }, [registrationsQuery.error, toast]);
+ const loadMajors = React.useCallback(async (params: { search: string; limit: number; offset: number }) => {
+ const data = await api.getMajorsPaged(params);
+ return { count: data.count, results: data.results.map((item) => ({ value: item.code, label: item.label })) };
+ }, []);
- if (loading) {
- return در حال بارگذاری...
;
- }
- if (!isAuthenticated || !(user?.is_staff || user?.is_superuser)) {
- return ;
- }
- if (!Number.isFinite(eventId)) {
- return شناسه رویداد معتبر نیست.
;
- }
+ const loadUniversities = React.useCallback(async (params: { search: string; limit: number; offset: number }) => {
+ const data = await api.getUniversitiesPaged(params);
+ return { count: data.count, results: data.results.map((item) => ({ value: item.code, label: item.label })) };
+ }, []);
+
+ if (loading) return در حال بارگذاری...
;
+ if (!isAuthenticated || !(user?.is_staff || user?.is_superuser)) return ;
+ if (!Number.isFinite(eventId)) return شناسه رویداد معتبر نیست.
;
const event = detailQuery.data;
const paged = registrationsQuery.data;
const registrationPageCount = paged ? Math.max(1, Math.ceil(paged.count / REGISTRATIONS_PAGE_SIZE)) : 1;
return (
-
-
-
-
-
{event?.title ?? 'جزئیات رویداد'}
- {event && (
-
- {event.status_label ?? event.status}
- {event.start_time ? شروع: {formatJalali(event.start_time)} : null}
- {event.event_type ? نوع: {event.event_type_label ?? event.event_type} : null}
-
- )}
-
-
-
-
-
+
+
+
+
{event?.title ?? "جزئیات رویداد"}
+ {event ?
شروع: {formatJalali(event.start_time)} · ثبتنامها: {toPersianDigits(event.registration_count)}
: null}
+
+
+
+
+
- {event && (
-
-
-
- وضعیت
- اطلاعات پایه رویداد
-
-
- ظرفیت: {event.capacity ?? 'نامحدود'}
- ثبتنامها: {toPersianDigits(event.registration_count ?? 0)}
- قیمت: {formatToman(event.price)}
-
-
-
-
- توضیحات
-
-
- {event.description || 'توضیحی ثبت نشده است.'}
-
-
-
- )}
+
+
+ {event ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+ در حال بارگذاری جزئیات...
+ )}
+
-
-
- ثبتنامها و پرداختها
- لیست ثبتنامهای مرتبط با این رویداد
-
-
-
-
-
-
{ setSearch(e.target.value); setRegPage(1); }}
- />
-
+ { setUniversity(value); setRegPage(1); }} loadOptions={loadUniversities} placeholder="دانشگاه" />
+ { setMajor(value); setRegPage(1); }} loadOptions={loadMajors} placeholder="رشته" />
- {registrationsQuery.isLoading ? (
- در حال بارگذاری ثبتنامها...
- ) : !paged || paged.results.length === 0 ? (
- ثبتنامی یافت نشد.
- ) : (
-
-
- {paged.results.map((registration) => (
-
-
-
-
{registration.user.first_name} {registration.user.last_name}
-
{registration.user.email}
-
-
- {registration.status_label}
-
-
-
-
نامکاربری: {registration.user.username}
-
کد بلیت: {registration.ticket_id}
-
تاریخ ثبتنام: {formatJalali(registration.registered_at)}
-
مبلغ پرداختی: {formatToman(registration.final_price ?? 0)}
-
تخفیف: {formatToman(registration.discount_amount ?? 0)}
-
- {registration.payments.length > 0 && (
-
-
پرداختها
- {registration.payments.map((payment) => (
-
- {payment.status_label}
- {formatToman(payment.amount)}
- Ref: {payment.ref_id ?? '—'}
-
- ))}
-
- )}
-
- ))}
-
-
- )}
-
-
-
صفحه {toPersianDigits(regPage)} از {toPersianDigits(registrationPageCount)}
-
-
-
+
+ {registrationsQuery.isLoading ? (
+
در حال بارگذاری...
+ ) : !paged || paged.results.length === 0 ? (
+
ثبتنامی یافت نشد.
+ ) : paged.results.map((registration) => (
+
+ ))}
-
-
-
+
+
+
صفحه {toPersianDigits(regPage)} از {toPersianDigits(registrationPageCount)}
+
+
+
+
+
+
+
+
+
+ { if (!open) setSelectedRegistration(null); }} />
);
}