import React, { useState, useEffect, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon, ShieldCheckIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import Portal from '../components/ui/Portal'; 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'; import SearchableSelect from '../components/ui/SearchableSelect'; import { XMarkIcon } from '@heroicons/react/24/outline'; // ── 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; } // ── Helpers ─────────────────────────────────────────────────────────────── const HUES_LIST = [256, 205, 162, 295, 272]; const ROLE_META: Record = { admin: { label: 'ادمین', badgeCls: 'violet' }, doctor: { label: 'پزشک', badgeCls: 'blue' }, secretary: { label: 'منشی', badgeCls: 'amber' }, clinic: { label: 'کلینیک', badgeCls: 'green' }, representation: { label: 'نماینده', badgeCls: 'red' }, patient: { label: 'بیمار', badgeCls: 'gray' }, }; // نقش‌هایی که از این صفحه قابل تغییرِ مستقیم‌اند (backend `roleMap`). «نماینده» از این‌جا ست نمی‌شود // چون ساخت نماینده رکورد Representation هم می‌خواهد؛ فقط برای نمایش badge/فیلتر تعریف شده است. const ASSIGNABLE_ROLES = ['admin', 'doctor', 'secretary', 'clinic', 'patient']; 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'; if (roles.includes('ROLE_REPRESENTATION')) return 'representation'; return 'patient'; } function UserAvatar({ name, id, size = 'sm' }: { name: string | null; id: number; size?: 'sm' | 'lg' }) { const initials = name ? name.split(' ').filter(Boolean).map((w) => w[0]).join('').toUpperCase().slice(0, 2) : '؟'; const hue = HUES_LIST[id % HUES_LIST.length]; 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 ? 'فعال' : 'غیرفعال'} ); } // ── 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 (
e.stopPropagation()}>

تغییر نقش

{user.name || user.mobile_number}

{Object.entries(ROLE_META).filter(([key]) => ASSIGNABLE_ROLES.includes(key)).map(([key, meta]) => ( ))}
); } // ── Constants ───────────────────────────────────────────────────────────── const ROLE_TABS = [ { key: '', label: 'همه نقش‌ها' }, { key: 'admin', label: 'ادمین' }, { key: 'doctor', label: 'پزشک' }, { key: 'secretary', label: 'منشی' }, { key: 'clinic', label: 'کلینیک' }, { key: 'representation', label: 'نماینده' }, { key: 'patient', label: 'بیمار' }, ]; // ── Main Page ───────────────────────────────────────────────────────────── export default function UsersPage() { const navigate = useNavigate(); const qc = useQueryClient(); const [page, setPage] = useState(1); const [limit] = useState(25); const [searchInput, setSearchInput] = useState(''); const [search, setSearch] = useState(''); const [role, setRole] = useState(''); const [status, setStatus] = useState(''); const [deleteTarget, setDeleteTarget] = useState(null); const [roleTarget, setRoleTarget] = useState(null); useEffect(() => { const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350); return () => clearTimeout(t); }, [searchInput]); useEffect(() => { setPage(1); }, [role, status]); // ── 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], queryFn: () => { const p = new URLSearchParams({ page: String(page), limit: String(limit) }); 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), }); // ── KPI data ── const kpiCards = [ { label: 'کل کاربران', value: stats?.total, bg: 'var(--surface-3)', color: 'var(--text-2)' }, { label: 'فعال', value: stats?.active, bg: 'var(--success-bg)', color: 'var(--success)' }, { label: 'غیرفعال', value: stats?.inactive, bg: 'var(--surface-3)', color: 'var(--text-3)' }, { label: 'ادمین‌ها', value: stats?.admins, bg: 'var(--violet-bg)', color: 'var(--violet)' }, { label: 'پزشکان', value: stats?.doctors, bg: 'var(--info-bg)', color: 'var(--info)' }, { label: 'بیماران', value: stats?.patients, bg: 'var(--warning-bg)', color: 'var(--warning)' }, ]; return (
{/* Header */}

کاربران

مدیریت کاربران، نقش‌ها و سطح دسترسی
{/* KPI cards */}
{kpiCards.map((c) => (
{c.label}
{c.value === undefined ? : formatNumber(c.value)}
))}
{/* Main card */}
{/* Toolbar */}
setSearchInput(e.target.value)} placeholder="نام، موبایل یا ایمیل..." />
({ value: t.key, label: t.label }))} value={role || ''} onChange={(v) => setRole(v ? String(v) : '')} height={36} />
{/* Table */}
{usersQ.isLoading && Array.from({ length: 7 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, j) => ( ))} ))} {!usersQ.isLoading && items.length === 0 && ( )} {!usersQ.isLoading && items.map((user) => ( ))}
کاربر شماره موبایل نقش تاریخ عضویت وضعیت
هیچ کاربری یافت نشد
navigate(`/admin/users/${user.uuid}`)}> {user.name || '—'}
#{user.id}
{user.mobile_number} {formatDate(user.created_at)}
{/* Dialogs */} deleteTarget && deleteMut.mutate(deleteTarget.uuid)} onCancel={() => setDeleteTarget(null)} /> {roleTarget && ( setRoleTarget(null)} onSave={(r) => updateRoleMut.mutate({ uuid: roleTarget.uuid, role: r })} /> )}
); }