import React, { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon, PlusIcon, EllipsisVerticalIcon, EyeIcon, PencilIcon, TrashIcon, ArrowPathIcon, UserGroupIcon, CheckCircleIcon, XCircleIcon, ShieldCheckIcon, HeartIcon, UserIcon, FunnelIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import { formatDate, formatNumber, maskMobile } from '../lib/utils'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Pagination from '../components/ui/Pagination'; // ── Types ───────────────────────────────────────────────────────────────── interface AdminUser { uuid: string; id: number; mobile_number: string; name: string | null; email: string | null; roles: string[]; is_active: boolean; status: number; created_at: string; } interface UserStats { total: number; active: number; inactive: number; admins: number; doctors: number; patients: number; } // ── Role 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' }, }; 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'; } // ── Sub-components ──────────────────────────────────────────────────────── const AVATAR_COLORS = [ 'bg-violet-100 text-violet-700 dark:bg-violet-400/20 dark:text-violet-300', 'bg-blue-100 text-blue-700 dark:bg-blue-400/20 dark:text-blue-300', 'bg-emerald-100 text-emerald-700 dark:bg-emerald-400/20 dark:text-emerald-300', 'bg-pink-100 text-pink-700 dark:bg-pink-400/20 dark:text-pink-300', 'bg-amber-100 text-amber-700 dark:bg-amber-400/20 dark:text-amber-300', ]; function Avatar({ 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 RoleBadge({ roles }: { roles: string[] }) { const key = getPrimaryRole(roles); const meta = ROLE_META[key]; return ( {meta.label} ); } function ActiveBadge({ active }: { active: boolean }) { return active ? ( فعال ) : ( غیرفعال ); } function highlight(text: string | null | undefined, query: string): React.ReactNode { if (!text || !query.trim()) return text ?? '—'; const idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return text; return ( <> {text.slice(0, idx)} {text.slice(idx, idx + query.length)} {text.slice(idx + query.length)} ); } // ── KPI Card ────────────────────────────────────────────────────────────── function KpiCard({ label, value, icon: Icon, iconBg, iconColor }: { label: string; value: number | undefined; icon: React.ElementType; iconBg: string; iconColor: string; }) { return (

{label}

{value === undefined ?
:

{formatNumber(value)}

}
); } // ── Role Change Modal ───────────────────────────────────────────────────── function ChangeRoleModal({ user, onClose, onSave, loading }: { user: AdminUser; onClose: () => void; onSave: (role: string) => void; loading: boolean; }) { const [selected, setSelected] = useState(getPrimaryRole(user.roles)); return (

تغییر نقش

{user.name || user.mobile_number}

{Object.entries(ROLE_META).map(([key, meta]) => ( ))}
); } // ── Constants ───────────────────────────────────────────────────────────── const ROLE_TABS = [ { key: '', label: 'همه', dot: '' }, { key: 'admin', label: 'ادمین', dot: '#8b5cf6' }, { key: 'doctor', label: 'پزشک', dot: '#3b82f6' }, { key: 'secretary', label: 'منشی', dot: '#f97316' }, { key: 'clinic', label: 'کلینیک', dot: '#10b981' }, { key: 'patient', label: 'بیمار', dot: '#94a3b8' }, ]; const LIMIT_OPTIONS = [10, 25, 50, 100]; // ── Main Page ───────────────────────────────────────────────────────────── export default function UsersPage() { const navigate = useNavigate(); const qc = useQueryClient(); const [page, setPage] = useState(1); const [limit, setLimit] = useState(25); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [role, setRole] = useState(''); const [status, setStatus] = useState(''); const [sort, setSort] = useState('newest'); const [selected, setSelected] = useState([]); const [activeMenu, setActiveMenu] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [roleTarget, setRoleTarget] = useState(null); const [confirmBulkDel, setConfirmBulkDel] = useState(false); const [bulkDeleting, setBulkDeleting] = useState(false); // Debounce search useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); // Reset page + selection when filters change useEffect(() => { setPage(1); setSelected([]); }, [role, status, sort, limit]); // Close action dropdown on outside click useEffect(() => { if (!activeMenu) return; const close = (e: MouseEvent) => { if ((e.target as HTMLElement).closest('[data-menu]') === null) setActiveMenu(null); }; document.addEventListener('mousedown', close); return () => document.removeEventListener('mousedown', close); }, [activeMenu]); // ── Queries ── const statsQ = useQuery({ queryKey: ['users-stats'], queryFn: () => api.get>('/api/v1/admin/users/stats'), staleTime: 30_000, }); const usersQ = useQuery({ queryKey: ['users', page, limit, search, role, status, sort], queryFn: () => { const p = new URLSearchParams({ page: String(page), limit: String(limit), sort }); if (search) p.set('search', search); if (role) p.set('role', role); if (status) p.set('status', status); return api.get>(`/api/v1/admin/users?${p}`); }, }); const stats = useMemo( () => (statsQ.data?.data as any)?.data ?? statsQ.data?.data, [statsQ.data] ); const items = usersQ.data?.data ?? []; const total = usersQ.data?.meta?.totalRecords ?? 0; // ── Mutations ── const deleteMut = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/admin/users/${uuid}`), onSuccess: () => { toast.success('کاربر حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['users'] }); qc.invalidateQueries({ queryKey: ['users-stats'] }); }, onError: (e: Error) => toast.error(e.message), }); const toggleStatusMut = useMutation({ mutationFn: (uuid: string) => api.post>(`/api/v1/admin/users/${uuid}/status`, {}), onSuccess: () => { toast.success('وضعیت کاربر تغییر کرد'); qc.invalidateQueries({ queryKey: ['users'] }); qc.invalidateQueries({ queryKey: ['users-stats'] }); }, onError: (e: Error) => toast.error(e.message), }); const updateRoleMut = useMutation({ mutationFn: ({ uuid, role: r }: { uuid: string; role: string }) => api.put>(`/api/v1/admin/users/${uuid}/role`, { role: r }), onSuccess: () => { toast.success('نقش کاربر بروزرسانی شد'); setRoleTarget(null); qc.invalidateQueries({ queryKey: ['users'] }); qc.invalidateQueries({ queryKey: ['users-stats'] }); }, onError: (e: Error) => toast.error(e.message), }); // ── Bulk actions ── const allSelected = items.length > 0 && items.every(u => selected.includes(u.uuid)); const toggleAll = () => setSelected(allSelected ? [] : items.map(u => u.uuid)); const toggleOne = (uuid: string) => setSelected(prev => prev.includes(uuid) ? prev.filter(id => id !== uuid) : [...prev, uuid]); const handleBulkDelete = async () => { setBulkDeleting(true); let count = 0; for (const uuid of selected) { try { await api.delete(`/api/v1/admin/users/${uuid}`); count++; } catch {} } setBulkDeleting(false); setConfirmBulkDel(false); setSelected([]); toast.success(`${formatNumber(count)} کاربر حذف شد`); qc.invalidateQueries({ queryKey: ['users'] }); qc.invalidateQueries({ queryKey: ['users-stats'] }); }; // ── KPI cards ── const kpiCards = [ { label: 'کل کاربران', value: stats?.total, icon: UserGroupIcon, iconBg: 'bg-slate-100 dark:bg-slate-400/10', iconColor: 'text-slate-600 dark:text-slate-400' }, { label: 'فعال', value: stats?.active, icon: CheckCircleIcon, iconBg: 'bg-green-100 dark:bg-green-400/10', iconColor: 'text-green-600 dark:text-green-400' }, { label: 'غیرفعال', value: stats?.inactive, icon: XCircleIcon, iconBg: 'bg-slate-100 dark:bg-slate-400/10', iconColor: 'text-slate-500 dark:text-slate-400' }, { label: 'ادمین‌ها', value: stats?.admins, icon: ShieldCheckIcon, iconBg: 'bg-violet-100 dark:bg-violet-400/10', iconColor: 'text-violet-600 dark:text-violet-400' }, { label: 'پزشکان', value: stats?.doctors, icon: HeartIcon, iconBg: 'bg-blue-100 dark:bg-blue-400/10', iconColor: 'text-blue-600 dark:text-blue-400' }, { label: 'بیماران', value: stats?.patients, icon: UserIcon, iconBg: 'bg-orange-100 dark:bg-orange-400/10', iconColor: 'text-orange-600 dark:text-orange-400' }, ]; const hasFilters = !!(search || role || status); return (
{/* ── Header ─────────────────────────────────────────────── */}

کاربران

مدیریت کاربران، نقش‌ها و سطح دسترسی سیستم

{/* ── KPI Cards ──────────────────────────────────────────── */}
{kpiCards.map((c) => )}
{/* ── Main Card ──────────────────────────────────────────── */}
{/* Toolbar */}
{/* Row 1: Search + Status + Sort + Limit + Refresh */}
setSearchInput(e.target.value)} placeholder="نام، موبایل یا ایمیل..." className="cp-input pr-9" /> {searchInput && ( )}
{/* Row 2: Role filter pills */}
نقش: {ROLE_TABS.map((t) => ( ))} {hasFilters && ( )}
{/* Bulk actions bar */} {selected.length > 0 && (
{formatNumber(selected.length)} کاربر انتخاب شده
)} {/* Table */}
{/* Loading skeleton */} {usersQ.isLoading && Array.from({ length: 8 }).map((_, i) => ( ))} {/* Empty state */} {!usersQ.isLoading && items.length === 0 && ( )} {/* Data rows */} {!usersQ.isLoading && items.map((user) => ( {/* Checkbox */} {/* Avatar + Name */} {/* Mobile */} {/* Email */} {/* Role */} {/* Status */} {/* Date */} {/* Actions dropdown */} ))}
کاربر موبایل ایمیل نقش وضعیت عضویت

{hasFilters ? 'نتیجه‌ای برای فیلترهای انتخابی یافت نشد' : 'هیچ کاربری ثبت نشده'}

{hasFilters && ( )}
e.stopPropagation()}> toggleOne(user.uuid)} className="rounded border-slate-300 dark:border-gray-600 text-primary-600 cursor-pointer" />
navigate(`/admin/users/${user.uuid}`)}>

{highlight(user.name, search) || '—'}

#{user.id}

{highlight(user.mobile_number, search) || maskMobile(user.mobile_number)} {user.email ? highlight(user.email, search) : '—'} {formatDate(user.created_at)} e.stopPropagation()}>
{activeMenu === user.uuid && (
)}
{/* Pagination */}
{/* Delete confirm */} deleteTarget && deleteMut.mutate(deleteTarget.uuid)} onCancel={() => setDeleteTarget(null)} /> {/* Bulk delete confirm */} setConfirmBulkDel(false)} /> {/* Role change modal */} {roleTarget && ( setRoleTarget(null)} onSave={(r) => updateRoleMut.mutate({ uuid: roleTarget.uuid, role: r })} /> )}
); }