1025 lines
42 KiB
TypeScript
1025 lines
42 KiB
TypeScript
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>
|
||
);
|
||
}
|
||
|