canina/frontend/admin-panel/src/pages/Users.tsx
parsaaghayi 8a3adacd38
Some checks failed
Deploy Canina / deploy (push) Successful in 33s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 8s
feat(admin): collapsible sidebar and responsive data tables with custom scroll
2026-09-23 23:45:18 +03:30

1025 lines
42 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,
Search,
Edit3,
Trash2,
Plus,
Eye,
Wallet,
AlertTriangle,
Mail,
Phone,
Lock,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Skeleton from '../components/ui/Skeleton';
import Pagination from '../components/ui/Pagination';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { UserAddress, PetSummary } from '../types/admin';
export interface UserRecord {
id: string;
firstName?: string;
lastName?: string;
mobile?: string;
email?: string;
role?: string;
status?: string;
walletBalance?: number | string;
charityDonationTotal?: number | string;
totalSpent?: number;
ordersCount?: number;
reviewsCount?: number;
petsCount?: number;
createdAt?: string;
addresses?: UserAddress[];
pets?: PetSummary[];
}
import { useSearchParams } from 'react-router-dom';
export default function Users() {
const [searchParams, setSearchParams] = useSearchParams();
const [users, setUsers] = useState<UserRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Queries
const [page, setPage] = useState(() => Number(searchParams.get('page')) || 1);
const [search, setSearch] = useState(() => searchParams.get('search') || '');
const [role, setRole] = useState(() => searchParams.get('role') || '');
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
// Modals
const [viewUser, setViewUser] = useState<UserRecord | null>(null);
const [editUser, setEditUser] = useState<UserRecord | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [userToDelete, setUserToDelete] = useState<UserRecord | null>(null);
const [walletUser, setWalletUser] = useState<UserRecord | null>(null);
const updateUrlParams = (paramsObj: Record<string, string | number | undefined | null>) => {
const current = Object.fromEntries(searchParams.entries());
const merged = { ...current, ...paramsObj };
const cleaned: Record<string, string> = {};
Object.entries(merged).forEach(([k, v]) => {
if (v !== undefined && v !== null && String(v).trim() !== '') cleaned[k] = String(v);
});
setSearchParams(cleaned, { replace: true });
};
// Form States
const [createFormData, setCreateFormData] = useState({
firstName: '',
lastName: '',
mobile: '',
email: '',
password: '',
role: 'User_PetOwner',
walletBalance: 0,
});
const [editFormData, setEditFormData] = useState({
firstName: '',
lastName: '',
email: '',
mobile: '',
password: '',
role: 'User_PetOwner',
walletBalance: 0,
});
const [walletFormData, setWalletFormData] = useState<{
amount: number;
type: 'deposit' | 'withdrawal' | 'refund';
description: string;
}>({
amount: 50000,
type: 'deposit',
description: '',
});
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isAdjustingWallet, setIsAdjustingWallet] = useState(false);
// Open View Modal
const handleOpenView = (user: UserRecord) => {
setViewUser(user);
updateUrlParams({ modal: 'view', userId: user.id });
};
const handleCloseView = () => {
setViewUser(null);
updateUrlParams({ modal: undefined, userId: undefined });
};
// Open Create Modal
const handleOpenCreate = () => {
setShowCreateModal(true);
updateUrlParams({ modal: 'create', userId: undefined });
};
const handleCloseCreate = () => {
setShowCreateModal(false);
updateUrlParams({ modal: undefined, userId: undefined });
};
// Open Edit Modal
const handleOpenEdit = (user: UserRecord) => {
setEditUser(user);
updateUrlParams({ modal: 'edit', userId: user.id });
setEditFormData({
firstName: user.firstName || '',
lastName: user.lastName || '',
email: user.email || '',
mobile: user.mobile || '',
password: '',
role: user.role || 'User_PetOwner',
walletBalance: Number(user.walletBalance) || 0,
});
};
const handleCloseEdit = () => {
setEditUser(null);
updateUrlParams({ modal: undefined, userId: undefined });
};
// Open Wallet Modal
const handleOpenWallet = (user: UserRecord) => {
setWalletUser(user);
updateUrlParams({ modal: 'wallet', userId: user.id });
setWalletFormData({
amount: 50000,
type: 'deposit',
description: '',
});
};
const handleCloseWallet = () => {
setWalletUser(null);
updateUrlParams({ modal: undefined, userId: undefined });
};
const handleCloseDelete = () => {
setUserToDelete(null);
};
const fetchUsers = useCallback(async () => {
try {
setIsLoading(true);
const params = new URLSearchParams();
params.append('page', page.toString());
params.append('limit', '10');
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) {
const userList = response.data.data || [];
setUsers(userList);
setTotalPages(response.data.meta?.lastPage || 1);
setTotalCount(response.data.meta?.total || 0);
const modalParam = searchParams.get('modal');
const userId = searchParams.get('userId');
if (modalParam === 'create') {
handleOpenCreate();
} else if (userId && userList.length > 0) {
const found = userList.find((u: UserRecord) => u.id === userId);
if (found) {
if (modalParam === 'view') handleOpenView(found);
else if (modalParam === 'edit') handleOpenEdit(found);
else if (modalParam === 'wallet') handleOpenWallet(found);
}
}
}
} catch (err) {
console.error('Failed to fetch users', err);
} finally {
setIsLoading(false);
}
}, [page, search, role, searchParams]);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
fetchUsers();
}, 400);
return () => clearTimeout(delayDebounceFn);
}, [fetchUsers]);
// Create User
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!createFormData.firstName.trim() || !createFormData.lastName.trim() || !createFormData.mobile.trim()) {
toast.error('لطفاً فیلدهای الزامی (نام، نام خانوادگی، شماره موبایل) را پر کنید');
return;
}
try {
setIsSaving(true);
const payload: Record<string, unknown> = {
firstName: createFormData.firstName.trim(),
lastName: createFormData.lastName.trim(),
mobile: createFormData.mobile.trim(),
role: createFormData.role,
walletBalance: Number(createFormData.walletBalance) || 0,
};
if (createFormData.email.trim()) payload.email = createFormData.email.trim();
if (createFormData.password.trim()) payload.password = createFormData.password.trim();
const res = await api.post('/admin/users', payload);
if (res.data?.success) {
toast.success('کاربر جدید با موفقیت ایجاد شد');
setShowCreateModal(false);
setCreateFormData({
firstName: '',
lastName: '',
mobile: '',
email: '',
password: '',
role: 'User_PetOwner',
walletBalance: 0,
});
fetchUsers();
}
} catch (err: unknown) {
console.error('Failed to create user', err);
} finally {
setIsSaving(false);
}
};
// Save Edit User
const handleSaveUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!editUser) return;
try {
setIsSaving(true);
const payload: Record<string, unknown> = {
firstName: editFormData.firstName.trim(),
lastName: editFormData.lastName.trim(),
mobile: editFormData.mobile.trim(),
role: editFormData.role,
walletBalance: Number(editFormData.walletBalance) || 0,
};
if (editFormData.email.trim()) payload.email = editFormData.email.trim();
if (editFormData.password.trim()) payload.password = editFormData.password.trim();
await api.put(`/admin/users/${editUser.id}`, payload);
toast.success('اطلاعات کاربر با موفقیت بروزرسانی شد');
setEditUser(null);
fetchUsers();
} catch (err: unknown) {
console.error('Failed to update user', err);
} finally {
setIsSaving(false);
}
};
// Delete User
const handleDeleteUser = async () => {
if (!userToDelete) return;
try {
setIsDeleting(true);
await api.delete(`/admin/users/${userToDelete.id}`);
toast.success('کاربر و تمامی داده‌های مربوطه با موفقیت حذف شدند');
setUserToDelete(null);
fetchUsers();
} catch (err: unknown) {
console.error('Failed to delete user', err);
} finally {
setIsDeleting(false);
}
};
// Adjust Wallet
const handleAdjustWallet = async (e: React.FormEvent) => {
e.preventDefault();
if (!walletUser) return;
try {
setIsAdjustingWallet(true);
await api.post(`/admin/users/${walletUser.id}/wallet-adjust`, {
amount: Number(walletFormData.amount),
type: walletFormData.type,
description: walletFormData.description || undefined,
});
toast.success('موجودی کیف پول با موفقیت تغییر کرد');
setWalletUser(null);
fetchUsers();
} catch (err: unknown) {
console.error('Failed to adjust wallet', err);
} finally {
setIsAdjustingWallet(false);
}
};
return (
<div className="space-y-6 font-vazir" dir="rtl">
{/* Header */}
<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">
افزودن، ویرایش، حذف، تغییر نقش و مدیریت مستقیم کیف پول ({totalCount.toLocaleString('fa-IR')} کاربر ثبتشده)
</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => setShowCreateModal(true)}
>
افزودن کاربر جدید
</Button>
<select
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"
value={role}
onChange={(e) => {
setRole(e.target.value);
setPage(1);
}}
>
<option value="">همه نقشها</option>
<option value="User_PetOwner">مشتری عادی</option>
<option value="User_Wholesale">خریدار عمده</option>
<option value="User_B2B">همکار (B2B)</option>
<option value="ADMIN">مدیر سیستم (Admin)</option>
</select>
<div className="relative w-full sm:w-64">
<Search className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="جستجو (نام، ایمیل، موبایل)..."
className="pl-4 pr-9 py-2.5 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium text-xs bg-white"
dir="rtl"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
/>
</div>
</div>
</div>
{/* Users Table */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden w-full">
<div className="overflow-x-auto custom-scrollbar w-full select-none cursor-grab active:cursor-grabbing pb-2">
<table className="w-full text-right font-vazir text-xs min-w-[1050px]">
<thead className="bg-gray-50 text-gray-500 border-b border-gray-200 sticky top-0 z-10">
<tr>
<th className="py-4 px-6 font-bold whitespace-nowrap">کاربر</th>
<th className="py-4 px-6 font-bold whitespace-nowrap">شماره موبایل</th>
<th className="py-4 px-6 font-bold whitespace-nowrap">نوع حساب</th>
<th className="py-4 px-6 font-bold whitespace-nowrap">مجموع خرید (LTV)</th>
<th className="py-4 px-6 font-bold text-center whitespace-nowrap">سفارشات</th>
<th className="py-4 px-6 font-bold text-center whitespace-nowrap"> مهربانی</th>
<th className="py-4 px-6 font-bold text-center whitespace-nowrap">دیدگاهها</th>
<th className="py-4 px-6 font-bold whitespace-nowrap">کیف پول</th>
<th className="py-4 px-6 font-bold whitespace-nowrap">تاریخ عضویت</th>
<th className="py-4 px-6 font-bold text-center whitespace-nowrap">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 text-xs font-vazir">
{isLoading ? (
<tr>
<td colSpan={10} className="py-12 text-center text-gray-400 font-bold">
در حال بارگذاری لیست کاربران...
</td>
</tr>
) : users.length === 0 ? (
<tr>
<td colSpan={10} className="py-12 text-center text-gray-400 font-bold">
هیچ کاربری با این مشخصات یافت نشد.
</td>
</tr>
) : (
users.map((user) => {
const fullName = `${user.firstName || ''} ${user.lastName || ''}`.trim() || 'بدون نام';
const balance = Number(user.walletBalance) || 0;
const charity = Number(user.charityDonationTotal) || 0;
const spent = Number(user.totalSpent) || 0;
const isMainAdmin = user.id === '12345678-1234-1234-1234-123456789012';
return (
<tr key={user.id} className="hover:bg-gray-50/70 transition-colors">
<td className="py-4 px-6 font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
<div className="w-8 h-8 rounded-full bg-purple-100 text-purple-700 flex items-center justify-center font-black text-xs shrink-0">
{fullName.charAt(0)}
</div>
<div className="whitespace-nowrap">
<div>{fullName}</div>
{isMainAdmin && (
<span className="text-[10px] text-purple-600 font-black">حساب اصلی مدیریت</span>
)}
</div>
</td>
<td className="py-4 px-6 font-mono text-gray-700 whitespace-nowrap" dir="ltr">
{user.mobile || '---'}
</td>
<td className="py-4 px-6 whitespace-nowrap">
<span
className={`px-2.5 py-1 rounded-full text-[10px] font-bold ${
user.role === 'User_Wholesale'
? 'bg-amber-100 text-amber-800'
: user.role?.includes('B2B')
? 'bg-blue-100 text-blue-700'
: user.role?.toLowerCase().includes('admin')
? 'bg-purple-100 text-purple-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{user.role === 'User_Wholesale'
? 'عمده'
: user.role?.includes('B2B')
? 'همکار B2B'
: user.role?.toLowerCase().includes('admin')
? 'مدیر'
: 'مشتری'}
</span>
</td>
<td className="py-4 px-6 font-mono font-bold text-emerald-700 whitespace-nowrap">
{spent > 0 ? `${spent.toLocaleString('fa-IR')} ت` : '۰'}
</td>
<td className="py-4 px-6 font-bold text-gray-800 whitespace-nowrap text-center">
<span className="bg-gray-100 px-2 py-0.5 rounded-md font-mono">
{user.ordersCount || 0}
</span>
</td>
<td className="py-4 px-6 font-mono font-bold text-rose-600 whitespace-nowrap text-center">
{charity > 0 ? `${charity.toLocaleString('fa-IR')} ت` : '---'}
</td>
<td className="py-4 px-6 font-bold text-purple-600 font-mono whitespace-nowrap text-center">
{user.reviewsCount || 0}
</td>
<td className="py-4 px-6 font-mono font-bold text-gray-800 whitespace-nowrap">
{balance > 0 ? `${balance.toLocaleString('fa-IR')} ت` : '۰'}
</td>
<td className="py-4 px-6 text-gray-500 text-xs font-medium whitespace-nowrap">
{user.createdAt ? new Date(user.createdAt).toLocaleDateString('fa-IR') : '---'}
</td>
<td className="py-4 px-6 text-center whitespace-nowrap">
<div className="flex justify-center items-center gap-1.5">
<button
onClick={() => setViewUser(user)}
className="p-1.5 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors cursor-pointer"
title="مشاهده جزئیات کامل"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleOpenWallet(user)}
className="p-1.5 text-emerald-600 hover:bg-emerald-50 rounded-lg transition-colors cursor-pointer"
title="شارژ / کسر کیف پول"
>
<Wallet className="w-4 h-4" />
</button>
<button
onClick={() => handleOpenEdit(user)}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
title="ویرایش مشخصات"
>
<Edit3 className="w-4 h-4" />
</button>
{!isMainAdmin && (
<button
onClick={() => setUserToDelete(user)}
className="p-1.5 text-red-500 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
title="حذف کاربر"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
</div>
{/* CREATE USER MODAL */}
{showCreateModal && (
<Modal
isOpen={showCreateModal}
onClose={handleCloseCreate}
title="افزودن کاربر جدید"
icon={Plus}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleCloseCreate}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
isLoading={isSaving}
onClick={handleCreateUser}
>
ایجاد کاربر
</Button>
</div>
}
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs font-vazir">
<div>
<label className="block text-gray-700 font-bold mb-1">
نام <span className="text-red-500">*</span>
</label>
<input
type="text"
required
placeholder="مثال: علی"
value={createFormData.firstName}
onChange={(e) => setCreateFormData({ ...createFormData, firstName: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">
نام خانوادگی <span className="text-red-500">*</span>
</label>
<input
type="text"
required
placeholder="مثال: احمدی"
value={createFormData.lastName}
onChange={(e) => setCreateFormData({ ...createFormData, lastName: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">
شماره موبایل <span className="text-red-500">*</span>
</label>
<div className="relative">
<Phone className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="tel"
required
placeholder="09123456789"
value={createFormData.mobile}
onChange={(e) => setCreateFormData({ ...createFormData, mobile: e.target.value })}
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
dir="ltr"
/>
</div>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">ایمیل (اختیاری)</label>
<div className="relative">
<Mail className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="email"
placeholder="user@example.com"
value={createFormData.email}
onChange={(e) => setCreateFormData({ ...createFormData, email: e.target.value })}
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
dir="ltr"
/>
</div>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">رمز عبور (اختیاری)</label>
<div className="relative">
<Lock className="w-4 h-4 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" />
<input
type="password"
placeholder="حداقل ۶ کاراکتر"
value={createFormData.password}
onChange={(e) => setCreateFormData({ ...createFormData, password: e.target.value })}
className="w-full pl-3.5 pr-10 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
dir="ltr"
/>
</div>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
<select
value={createFormData.role}
onChange={(e) => setCreateFormData({ ...createFormData, role: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
>
<option value="User_PetOwner">مشتری عادی</option>
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
<option value="User_B2B">همکار (B2B)</option>
<option value="ADMIN">مدیر سیستم (Admin)</option>
</select>
</div>
<div className="sm:col-span-2">
<label className="block text-gray-700 font-bold mb-1">موجودی اولیه کیف پول (تومان)</label>
<input
type="number"
min="0"
step="1000"
placeholder="0"
value={createFormData.walletBalance}
onChange={(e) => setCreateFormData({ ...createFormData, walletBalance: Number(e.target.value) })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
/>
</div>
</div>
</Modal>
)}
{/* EDIT USER MODAL */}
{editUser && (
<Modal
isOpen={!!editUser}
onClose={handleCloseEdit}
title="ویرایش اطلاعات کاربر"
icon={Edit3}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleCloseEdit}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
isLoading={isSaving}
onClick={handleSaveUser}
>
ذخیره تغییرات
</Button>
</div>
}
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs font-vazir">
<div>
<label className="block text-gray-700 font-bold mb-1">نام</label>
<input
type="text"
required
value={editFormData.firstName}
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">نام خانوادگی</label>
<input
type="text"
required
value={editFormData.lastName}
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">شماره موبایل</label>
<input
type="tel"
required
value={editFormData.mobile}
onChange={(e) => setEditFormData({ ...editFormData, mobile: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
dir="ltr"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">ایمیل</label>
<input
type="email"
value={editFormData.email}
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-left"
dir="ltr"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">رمز عبور جدید (در صورت نیاز به تغییر)</label>
<input
type="password"
placeholder="بدون تغییر..."
value={editFormData.password}
onChange={(e) => setEditFormData({ ...editFormData, password: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-left"
dir="ltr"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">نقش دسترسی</label>
<select
value={editFormData.role}
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-bold bg-white"
>
<option value="User_PetOwner">مشتری عادی</option>
<option value="User_Wholesale">خریدار عمده (داروخانه/پتشاپ)</option>
<option value="User_B2B">همکار (B2B)</option>
<option value="ADMIN">مدیر سیستم (Admin)</option>
</select>
</div>
<div className="sm:col-span-2">
<label className="block text-gray-700 font-bold mb-1">موجودی کیف پول (تومان)</label>
<input
type="number"
min="0"
step="1000"
value={editFormData.walletBalance}
onChange={(e) => setEditFormData({ ...editFormData, walletBalance: Number(e.target.value) })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono"
/>
</div>
</div>
</Modal>
)}
{/* WALLET ADJUST MODAL */}
{walletUser && (
<Modal
isOpen={!!walletUser}
onClose={handleCloseWallet}
title="مدیریت کیف پول کاربر"
icon={Wallet}
maxWidth="md"
footer={
<div className="flex justify-end gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleCloseWallet}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
isLoading={isAdjustingWallet}
onClick={handleAdjustWallet}
>
ثبت تغییر موجودی
</Button>
</div>
}
>
<div className="space-y-4 text-xs font-vazir">
<div className="bg-gray-50 p-3.5 rounded-2xl border border-gray-100 space-y-1">
<div className="font-bold text-gray-900">
{walletUser.firstName} {walletUser.lastName} ({walletUser.mobile})
</div>
<div className="text-gray-500">
موجودی فعلی:{' '}
<span className="font-bold text-emerald-600 font-mono">
{(Number(walletUser.walletBalance) || 0).toLocaleString('fa-IR')} تومان
</span>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-gray-700 font-bold mb-1.5">نوع عملیات</label>
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={() => setWalletFormData({ ...walletFormData, type: 'deposit' })}
className={`py-2 rounded-xl font-bold border transition-all cursor-pointer ${
walletFormData.type === 'deposit'
? 'bg-emerald-50 border-emerald-500 text-emerald-700'
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
افزایش (شارژ)
</button>
<button
type="button"
onClick={() => setWalletFormData({ ...walletFormData, type: 'withdrawal' })}
className={`py-2 rounded-xl font-bold border transition-all cursor-pointer ${
walletFormData.type === 'withdrawal'
? 'bg-red-50 border-red-500 text-red-700'
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
کاهش (کسر)
</button>
<button
type="button"
onClick={() => setWalletFormData({ ...walletFormData, type: 'refund' })}
className={`py-2 rounded-xl font-bold border transition-all cursor-pointer ${
walletFormData.type === 'refund'
? 'bg-blue-50 border-blue-500 text-blue-700'
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
استرداد وجه
</button>
</div>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">مبلغ (تومان)</label>
<input
type="number"
min="1000"
step="1000"
value={walletFormData.amount}
onChange={(e) => setWalletFormData({ ...walletFormData, amount: Number(e.target.value) })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono font-bold text-sm"
/>
</div>
<div>
<label className="block text-gray-700 font-bold mb-1">توضیحات تراکنش (اختیاری)</label>
<input
type="text"
placeholder="مثال: هدیه ثبت نام، تسویه دستی..."
value={walletFormData.description}
onChange={(e) => setWalletFormData({ ...walletFormData, description: e.target.value })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
/>
</div>
</div>
</div>
</Modal>
)}
{/* DELETE CONFIRMATION MODAL */}
{userToDelete && (
<Modal
isOpen={!!userToDelete}
onClose={handleCloseDelete}
title="تایید حذف کاربر"
icon={AlertTriangle}
maxWidth="md"
footer={
<div className="flex justify-end gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleCloseDelete}
>
انصراف
</Button>
<Button
variant="danger"
size="sm"
isLoading={isDeleting}
onClick={handleDeleteUser}
>
حذف قطعی کاربر
</Button>
</div>
}
>
<div className="space-y-3 text-xs font-vazir">
<p className="text-gray-700 leading-relaxed">
آیا از حذف کامل کاربر{' '}
<strong className="text-gray-900">
{userToDelete.firstName} {userToDelete.lastName} ({userToDelete.mobile})
</strong>{' '}
اطمینان دارید؟ تمام سوابق آدرسها، پتها و تراکنشهای مربوطه نیز پاک خواهند شد.
</p>
</div>
</Modal>
)}
{/* VIEW USER DETAILS MODAL */}
{viewUser && (
<Modal
isOpen={!!viewUser}
onClose={handleCloseView}
title="مشاهده جزئیات پروفایل کاربر"
icon={UsersIcon}
maxWidth="lg"
footer={
<div className="flex justify-end w-full">
<Button
variant="secondary"
size="sm"
onClick={() => setViewUser(null)}
>
بستن پنجره
</Button>
</div>
}
>
<div className="space-y-4 text-xs font-vazir">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-2xl border border-gray-100">
<div>
<span className="text-gray-400 font-bold block mb-1">نام و نام خانوادگی:</span>
<span className="font-bold text-gray-900 text-sm">
{viewUser.firstName} {viewUser.lastName}
</span>
</div>
<div>
<span className="text-gray-400 font-bold block mb-1">نوع نقش:</span>
<span className="font-bold text-purple-700">{viewUser.role}</span>
</div>
<div>
<span className="text-gray-400 font-bold block mb-1">موبایل:</span>
<span className="font-mono text-gray-800" dir="ltr">
{viewUser.mobile || '---'}
</span>
</div>
<div>
<span className="text-gray-400 font-bold block mb-1">ایمیل:</span>
<span className="font-mono text-gray-800" dir="ltr">
{viewUser.email || '---'}
</span>
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
<div className="p-3 bg-emerald-50 rounded-2xl border border-emerald-100 text-center">
<span className="text-[10px] font-black text-emerald-800 block">مجموع خرید (LTV)</span>
<span className="text-xs font-black text-emerald-700 font-mono mt-0.5 block">
{(Number(viewUser.totalSpent) || 0).toLocaleString('fa-IR')} تومان
</span>
</div>
<div className="p-3 bg-blue-50 rounded-2xl border border-blue-100 text-center">
<span className="text-[10px] font-black text-blue-800 block">تعداد سفارشات</span>
<span className="text-sm font-black text-blue-700 font-mono mt-0.5 block">
{viewUser.ordersCount || 0}
</span>
</div>
<div className="p-3 bg-rose-50 rounded-2xl border border-rose-100 text-center">
<span className="text-[10px] font-black text-rose-800 block"> پویش مهربانی</span>
<span className="text-xs font-black text-rose-700 font-mono mt-0.5 block">
{(Number(viewUser.charityDonationTotal) || 0).toLocaleString('fa-IR')} تومان
</span>
</div>
<div className="p-3 bg-purple-50 rounded-2xl border border-purple-100 text-center">
<span className="text-[10px] font-black text-purple-800 block">دیدگاهها و نظرات</span>
<span className="text-sm font-black text-purple-700 font-mono mt-0.5 block">
{viewUser.reviewsCount || 0}
</span>
</div>
</div>
<div className="bg-purple-50/60 p-4 rounded-2xl border border-purple-100 flex items-center justify-between">
<span className="font-bold text-purple-900">موجودی کیف پول کاربر:</span>
<span className="font-mono font-black text-purple-700 text-base">
{(Number(viewUser.walletBalance) || 0).toLocaleString('fa-IR')} تومان
</span>
</div>
<div>
<h4 className="font-bold text-gray-900 mb-2">آدرسهای ثبتشده ({viewUser.addresses?.length || 0})</h4>
{viewUser.addresses && viewUser.addresses.length > 0 ? (
<div className="space-y-2 max-h-36 overflow-y-auto">
{viewUser.addresses.map((a: UserAddress, idx: number) => (
<div key={idx} className="p-2.5 bg-gray-50 rounded-xl border border-gray-200/60">
<p className="font-bold text-gray-800">
{a.title} - {a.receptorName}
</p>
<p className="text-gray-500 mt-0.5">
{a.province}، {a.city}، {a.detail}
</p>
</div>
))}
</div>
) : (
<p className="text-gray-400 text-center py-3 bg-gray-50 rounded-xl">هیچ آدرسی ثبت نشده است.</p>
)}
</div>
</div>
</Modal>
)}
</div>
);
}