canina/frontend/admin-panel/src/pages/Users.tsx

214 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { Users as UsersIcon, CheckCircle, Search, Edit3, Check, X } from 'lucide-react';
import api from '../services/api';
import Skeleton from '../components/ui/Skeleton';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
export interface UserRecord {
[x: string]: string;
id: string;
firstName?: string;
lastName?: string;
mobile?: string;
email?: string;
role?: string;
createdAt?: string;
}
export default function Users() {
const [users, setUsers] = useState<UserRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Queries
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [role, setRole] = useState('');
const [totalPages, setTotalPages] = useState(1);
// Edit State
const [editingId, setEditingId] = useState<string | null>(null);
const [editRole, setEditRole] = useState<string>('');
const [isSaving, setIsSaving] = useState(false);
const fetchUsers = useCallback(async () => {
try {
setIsLoading(true);
const params = new URLSearchParams();
params.append('page', page.toString());
if (search) params.append('search', search);
if (role) params.append('role', role);
const response = await api.get(`/admin/users?${params.toString()}`);
if (response.data?.success) {
setUsers(response.data.data);
setTotalPages(response.data.meta?.lastPage || 1);
}
} catch (err) {
console.error('Failed to fetch users', err);
} finally {
setIsLoading(false);
}
}, [page, search, role]);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
fetchUsers();
}, 500);
return () => clearTimeout(delayDebounceFn);
}, [fetchUsers]);
const handleEditClick = (user: UserRecord) => {
setEditingId(user.id);
setEditRole(user.role || 'User_PetOwner');
};
const handleSaveEdit = async (id: string) => {
try {
setIsSaving(true);
await api.put(`/admin/users/${id}/role`, { role: editRole });
setEditingId(null);
fetchUsers();
} catch (err) {
console.error('Failed to update user role', err);
} finally {
setIsSaving(false);
}
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<UsersIcon className="w-6 h-6 text-purple-600" />
مدیریت کاربران
</h2>
<p className="text-gray-500 font-medium mt-1">مشاهده و تایید حساب‌های کاربری و کلینیک‌ها</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
<select
className="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto"
value={role}
onChange={(e) => { setRole(e.target.value); setPage(1); }}
>
<option value="">همه نقش‌ها</option>
<option value="B2B">همکار (B2B)</option>
<option value="User_PetOwner">مشتری عادی</option>
<option value="ADMIN">ادمین</option>
</select>
<div className="relative w-full sm:w-64">
<Search className="w-5 h-5 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="جستجو کاربر (نام، ایمیل، موبایل)..."
className="pl-4 pr-10 py-2 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium"
dir="rtl"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
</div>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right font-vazir">
<thead className="bg-gray-50 text-gray-500 text-sm border-b border-gray-200">
<tr>
<th className="py-4 px-6 font-bold">نام کاربر / کلینیک</th>
<th className="py-4 px-6 font-bold">ایمیل</th>
<th className="py-4 px-6 font-bold">نوع حساب</th>
<th className="py-4 px-6 font-bold">تاریخ عضویت</th>
<th className="py-4 px-6 font-bold">وضعیت</th>
<th className="py-4 px-6 font-bold text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => (
<tr key={i}>
<td className="py-4 px-6"><Skeleton className="h-6 w-32" /></td>
<td className="py-4 px-6"><Skeleton className="h-6 w-40" /></td>
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
<td className="py-4 px-6"><Skeleton className="h-6 w-20" /></td>
<td className="py-4 px-6"><Skeleton className="h-6 w-24" /></td>
<td className="py-4 px-6"><Skeleton className="h-8 w-20 mx-auto" /></td>
</tr>
))
) : (
users.map((user) => {
const isEditing = editingId === user.id;
return (
<tr key={user.id} className="hover:bg-gray-50/50 transition-colors">
<td className="py-4 px-6 font-bold text-gray-900">{user.firstName} {user.lastName}</td>
<td className="py-4 px-6 text-gray-500" dir="ltr">{user.email}</td>
<td className="py-4 px-6">
{isEditing ? (
<select
className="border border-purple-300 rounded p-1 text-xs font-bold"
value={editRole}
onChange={(e) => setEditRole(e.target.value)}
>
<option value="User_PetOwner">مشتری عادی</option>
<option value="User_Wholesale">خریدار عمده (تاییدشده)</option>
<option value="User_B2B">همکار (B2B)</option>
<option value="ADMIN">ادمین</option>
</select>
) : (
<span className={`px-3 py-1 rounded-full text-xs font-bold ${user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
{user.role === 'User_Wholesale' ? 'خریدار عمده' :
user.role?.includes('B2B') ? 'همکار (B2B)' :
(user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
</span>
)}
</td>
<td className="py-4 px-6 text-gray-500 text-sm font-medium">{new Date(user.createdAt).toLocaleDateString('fa-IR')}</td>
<td className="py-4 px-6">
{user.status === 'active' ? (
<span className="flex items-center gap-1.5 text-green-600 font-bold text-sm">
<CheckCircle className="w-4 h-4" />
تایید شده
</span>
) : (
<span className="flex items-center gap-1.5 text-orange-500 font-bold text-sm">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
در انتظار تایید
</span>
)}
</td>
<td className="py-4 px-6 text-center">
{isEditing ? (
<div className="flex justify-center gap-2">
<button onClick={() => handleSaveEdit(user.id)} className="text-green-600 hover:bg-green-50 p-1.5 rounded-lg transition-colors" disabled={isSaving}>
{isSaving ? <Spinner size="sm" /> : <Check className="w-5 h-5" />}
</button>
<button onClick={() => setEditingId(null)} className="text-gray-500 hover:bg-gray-50 p-1.5 rounded-lg transition-colors" disabled={isSaving}>
<X className="w-5 h-5" />
</button>
</div>
) : (
<div className="flex justify-center gap-2">
<button onClick={() => handleEditClick(user)} className="text-blue-600 hover:bg-blue-50 p-1.5 rounded-lg transition-colors" title="ویرایش نقش">
<Edit3 className="w-5 h-5" />
</button>
</div>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
</div>
</div>
);
}