import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ArrowRightIcon, PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, ShieldCheckIcon, UserIcon, PhoneIcon, EnvelopeIcon, CalendarIcon, KeyIcon, EllipsisVerticalIcon, ClipboardDocumentIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { formatDate, formatDateTime } from '../lib/utils'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; // ── Types ───────────────────────────────────────────────────────────────── interface AdminUserDetail { uuid: string; id: number; mobile_number: string; name: string | null; email: string | null; roles: string[]; is_active: boolean; status: number; created_at: string; updated_at: string; } interface UserProfileData { uuid: string; national_code: string | null; gender: string | null; birthday: number | null; blood_type: string | null; marital_status: string | null; education: string | null; job: string | null; fathers_name: string | null; address: string | null; home_phone: string | null; work_phone: string | null; basic_insurance_id: number | null; supplementary_insurance_id: number | null; other: Record | null; } const GENDER_LABELS: Record = { male: 'مرد', female: 'زن' }; const MARITAL_LABELS: Record = { married: 'متاهل', single: 'مجرد' }; // Each medical section stores objects with its own shape; format an item to a label. const MEDICAL_SECTIONS: { key: string; label: string; format: (item: any) => string; filter?: (item: any) => boolean; }[] = [ { key: 'allergies', label: 'آلرژی‌ها', format: (i) => [i.substance, i.reaction, i.severity].filter(Boolean).join(' — ') }, { key: 'medications', label: 'داروهای مصرفی', format: (i) => [i.name, i.dose, i.frequency].filter(Boolean).join(' — ') }, { key: 'surgeries', label: 'جراحی‌ها', format: (i) => [i.type, i.year && `سال ${i.year}`, i.hospital].filter(Boolean).join(' — ') }, { key: 'family_history', label: 'سابقه خانوادگی بیماری', format: (i) => [i.relation, i.disease].filter(Boolean).join(': ') }, // disease is a fixed checklist; only show the ones marked active { key: 'disease', label: 'بیماری‌ها', format: (i) => i.name, filter: (i) => i.status === 'true' || i.status === true }, ]; // ── Helpers ─────────────────────────────────────────────────────────────── const ROLE_META: Record = { admin: { label: 'ادمین', cls: 'bg-violet-100 dark:bg-violet-400/10 text-violet-700 dark:text-violet-300', dot: '#8b5cf6' }, doctor: { label: 'پزشک', cls: 'bg-blue-100 dark:bg-blue-400/10 text-blue-700 dark:text-blue-300', dot: '#3b82f6' }, secretary: { label: 'منشی', cls: 'bg-orange-100 dark:bg-orange-400/10 text-orange-700 dark:text-orange-300', dot: '#f97316' }, clinic: { label: 'کلینیک', cls: 'bg-emerald-100 dark:bg-emerald-400/10 text-emerald-700 dark:text-emerald-300', dot: '#10b981' }, patient: { label: 'بیمار', cls: 'bg-slate-100 dark:bg-slate-400/10 text-slate-600 dark:text-slate-400', dot: '#94a3b8' }, }; const ROLE_MAP: Record = { admin: ['ROLE_USER', 'ROLE_ADMIN'], doctor: ['ROLE_USER', 'ROLE_DOCTOR'], secretary: ['ROLE_USER', 'ROLE_SECRETARY'], clinic: ['ROLE_USER', 'ROLE_CLINIC'], patient: ['ROLE_USER'], }; function getPrimaryRole(roles: string[]): string { if (roles.includes('ROLE_ADMIN')) return 'admin'; if (roles.includes('ROLE_DOCTOR')) return 'doctor'; if (roles.includes('ROLE_SECRETARY')) return 'secretary'; if (roles.includes('ROLE_CLINIC')) return 'clinic'; return 'patient'; } const AVATAR_COLORS = [ 'from-violet-500 to-purple-600', 'from-blue-500 to-cyan-600', 'from-emerald-500 to-teal-600', 'from-pink-500 to-rose-600', 'from-amber-500 to-orange-600', ]; function BigAvatar({ name, id }: { name: string | null; id: number }) { const initials = name ? name.split(' ').filter(Boolean).map(w => w[0]).join('').toUpperCase().slice(0, 2) : '؟'; return (
{initials}
); } function InfoCard({ icon: Icon, label, value, mono = false, copyable = false }: { icon: React.ElementType; label: string; value: React.ReactNode; mono?: boolean; copyable?: boolean; }) { const handleCopy = () => { if (typeof value === 'string') { navigator.clipboard.writeText(value); toast.success('کپی شد'); } }; return (

{label}

{value ?? ثبت نشده}

{copyable && typeof value === 'string' && ( )}
); } // ── Edit schema ─────────────────────────────────────────────────────────── const editSchema = z.object({ name: z.string().max(100).optional().or(z.literal('')), email: z.string().email('ایمیل نامعتبر').optional().or(z.literal('')), password: z.string().min(6, 'رمز عبور حداقل ۶ کاراکتر').optional().or(z.literal('')), }); type EditForm = z.infer; // ── Role Change Modal ───────────────────────────────────────────────────── function ChangeRoleModal({ current, loading, onSave, onClose }: { current: string; loading: boolean; onSave: (role: string) => void; onClose: () => void; }) { const [selected, setSelected] = useState(current); return ( } >
{Object.entries(ROLE_META).map(([key, meta]) => ( ))}
); } // ── Main Page ───────────────────────────────────────────────────────────── export default function UserDetailPage() { const { uuid } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const qc = useQueryClient(); const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1'); const [roleOpen, setRoleOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { if (!menuOpen) return; const close = (e: MouseEvent) => { if ((e.target as HTMLElement).closest('[data-upmenu]') === null) setMenuOpen(false); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [menuOpen]); // ── Query ── const { data, isLoading, isError } = useQuery({ queryKey: ['admin-user', uuid], queryFn: () => api.get>(`/api/v1/admin/users/${uuid}`), enabled: !!uuid, }); const user: AdminUserDetail | undefined = (data?.data as any)?.data ?? data?.data; const profileQ = useQuery({ queryKey: ['admin-user-profile', uuid], queryFn: () => api.get>(`/api/v1/user-profile/${uuid}`), enabled: !!uuid, retry: false, }); const profile: UserProfileData | undefined = (profileQ.data as any)?.data?.data ?? (profileQ.data as any)?.data; // ── Edit form ── const { register, handleSubmit, reset, formState: { errors } } = useForm({ resolver: zodResolver(editSchema), }); useEffect(() => { if (user) reset({ name: user.name ?? '', email: user.email ?? '', password: '' }); }, [user, reset]); // ── Mutations ── const updateMut = useMutation({ mutationFn: (body: EditForm) => api.put>(`/api/v1/admin/users/${uuid}`, body), onSuccess: () => { toast.success('اطلاعات بروزرسانی شد'); setEditOpen(false); qc.invalidateQueries({ queryKey: ['admin-user', uuid] }); qc.invalidateQueries({ queryKey: ['users'] }); }, onError: (e: Error) => toast.error(e.message), }); const toggleStatusMut = useMutation({ mutationFn: () => api.post>(`/api/v1/admin/users/${uuid}/status`, {}), onSuccess: () => { toast.success('وضعیت کاربر تغییر کرد'); qc.invalidateQueries({ queryKey: ['admin-user', uuid] }); qc.invalidateQueries({ queryKey: ['users'] }); }, onError: (e: Error) => toast.error(e.message), }); const updateRoleMut = useMutation({ mutationFn: (role: string) => api.put>(`/api/v1/admin/users/${uuid}/role`, { role }), onSuccess: () => { toast.success('نقش کاربر بروزرسانی شد'); setRoleOpen(false); qc.invalidateQueries({ queryKey: ['admin-user', uuid] }); qc.invalidateQueries({ queryKey: ['users'] }); }, onError: (e: Error) => toast.error(e.message), }); const deleteMut = useMutation({ mutationFn: () => api.delete>(`/api/v1/admin/users/${uuid}`), onSuccess: () => { toast.success('کاربر حذف شد'); navigate('/admin/users'); }, onError: (e: Error) => toast.error(e.message), }); // ── Loading skeleton ── if (isLoading) { return (
{Array.from({ length: 6 }).map((_, i) =>
)}
); } if (isError || !user) { return (

کاربر یافت نشد

این کاربر حذف شده یا UUID اشتباه است.

); } const primaryRole = getPrimaryRole(user.roles); const roleMeta = ROLE_META[primaryRole]; return (
{/* ── Breadcrumb / Back ─────────────────────────────────── */}
/ پروفایل کاربر
{/* ── Profile Card ──────────────────────────────────────── */}
{/* Cover strip */}
{/* Profile header */}

{user.name || نام ثبت نشده}

{roleMeta.label} {user.is_active ? ( فعال ) : ( غیرفعال )} #{user.id}
{/* Action buttons */}
{/* More menu */}
{menuOpen && (
)}
{/* ── Info Grid ─────────────────────────────────────────── */}
{/* Left: Contact info */}

اطلاعات تماس و حساب

{/* Right: Roles + UUID */}
{/* Roles card */}

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

{user.roles.map((r) => ( {r} ))}
{/* UUID card */}

شناسه یکتا (UUID)

{user.uuid}

{/* Status card */}

وضعیت حساب

{user.is_active ? فعال : غیرفعال }
{/* ── User Profile (read-only) ──────────────────────────── */}

پروفایل کاربر

{profileQ.isLoading ? (
{Array.from({ length: 6 }).map((_, i) =>
)}
) : !profile ? (

این کاربر هنوز پروفایلی تکمیل نکرده است.

) : ( <>
{/* Medical history */}

سوابق پزشکی

{MEDICAL_SECTIONS.map(({ key, label, format, filter }) => { const raw = Array.isArray(profile.other?.[key]) ? (profile.other![key] as any[]) : []; const items = (filter ? raw.filter(filter) : raw) .map(format) .filter((s) => s && s.trim()); return (

{label}

{items.length === 0 ? (

موردی ثبت نشده

) : (
    {items.map((text, i) => (
  • {text}
  • ))}
)}
); })}
)}
{/* ── Edit Modal ────────────────────────────────────────── */} setEditOpen(false)} footer={ <> } >
updateMut.mutate(v))} className="space-y-4"> {/* Avatar preview */}

{user.name || 'نام ثبت نشده'}

{user.mobile_number}

{/* Name */}
{errors.name &&

{errors.name.message}

}
{/* Email */}
{errors.email &&

{errors.email.message}

}
{/* Password */}
{errors.password &&

{errors.password.message}

}
{/* Mobile (read-only) */}
{/* ── Role change modal ─────────────────────────────────── */} {roleOpen && ( updateRoleMut.mutate(r)} onClose={() => setRoleOpen(false)} /> )} {/* ── Delete confirm ────────────────────────────────────── */} deleteMut.mutate()} onCancel={() => setDeleteOpen(false)} />
); }