fix(users): update user role and edit API logic
This commit is contained in:
@@ -1,62 +1,92 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
import DataTable from '../components/DataTable';
|
import DataTable from '../components/DataTable';
|
||||||
import { usersAPI } from '../services/api';
|
import { usersAPI, rolesAPI } from '../services/api';
|
||||||
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
|
// debounce ساده
|
||||||
|
function debounce(func, delay) {
|
||||||
|
let timer;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => func.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [firstName, setFirstName] = useState('');
|
|
||||||
const [lastName, setLastName] = useState('');
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
|
const [addForm, setAddForm] = useState({ firstName: '', lastName: '', email: '' });
|
||||||
|
const [editForm, setEditForm] = useState({ firstName: '', lastName: '', email: '' });
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState(null);
|
const [editingUser, setEditingUser] = useState(null);
|
||||||
|
|
||||||
const fetchUsers = async (q = '') => {
|
const [roles, setRoles] = useState([]);
|
||||||
|
const [rolesModalUser, setRolesModalUser] = useState(null);
|
||||||
|
const [selectedRoles, setSelectedRoles] = useState([]);
|
||||||
|
|
||||||
|
// دریافت کاربران و رولها
|
||||||
|
const fetchUsers = useCallback(async (q = '') => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const list = await usersAPI.list(q);
|
const list = await usersAPI.list({ searchQuery: q });
|
||||||
setUsers(Array.isArray(list) ? list : []);
|
const usersWithRoles = await Promise.all(
|
||||||
} catch (e) {
|
(list || []).map(async (user) => {
|
||||||
|
const roles = await usersAPI.getRoles(user.id); // باید طبق ریسپانس نمونه برگرداند
|
||||||
|
return { ...user, roles };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setUsers(usersWithRoles);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
setError('Failed to load users');
|
setError('Failed to load users');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
const fetchRoles = useCallback(async () => {
|
||||||
|
const list = await rolesAPI.list(); // گرفتن تمام رولها
|
||||||
|
setRoles(list);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}, []);
|
fetchRoles();
|
||||||
|
}, [fetchUsers, fetchRoles]);
|
||||||
|
|
||||||
|
// --- اضافه کردن کاربر ---
|
||||||
const onAddUser = async (e) => {
|
const onAddUser = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const { firstName, lastName, email } = addForm;
|
||||||
if (!firstName.trim() || !lastName.trim() || !email.trim()) return;
|
if (!firstName.trim() || !lastName.trim() || !email.trim()) return;
|
||||||
try {
|
try {
|
||||||
await usersAPI.create({ firstName, lastName, email });
|
const newUser = await usersAPI.create(addForm);
|
||||||
setFirstName('');
|
|
||||||
setLastName('');
|
|
||||||
setEmail('');
|
|
||||||
await fetchUsers(filter);
|
await fetchUsers(filter);
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
} catch (_) {
|
setAddForm({ firstName: '', lastName: '', email: '' });
|
||||||
setError('Failed to create user');
|
toast.success(newUser?.Message || 'User added successfully');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to create user';
|
||||||
|
toast.error(msg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = async (id) => {
|
// --- ویرایش کاربر ---
|
||||||
if (!window.confirm('Are you sure you want to delete this user?')) return;
|
|
||||||
await usersAPI.remove(id);
|
|
||||||
await fetchUsers(filter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openEdit = (user) => {
|
const openEdit = (user) => {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setFirstName(user.firstName || '');
|
setEditForm({
|
||||||
setLastName(user.lastName || '');
|
firstName: user.firstName || '',
|
||||||
setEmail(user.email || '');
|
lastName: user.lastName || '',
|
||||||
|
email: user.email || '',
|
||||||
|
});
|
||||||
setIsEditModalOpen(true);
|
setIsEditModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,205 +94,227 @@ const Users = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!editingUser) return;
|
if (!editingUser) return;
|
||||||
try {
|
try {
|
||||||
await usersAPI.update(editingUser.id, { firstName, lastName, email });
|
const updatedUser = await usersAPI.update(editingUser.id, editForm);
|
||||||
await fetchUsers(filter);
|
await fetchUsers(filter);
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
setFirstName('');
|
toast.success(updatedUser?.Message || 'User updated successfully');
|
||||||
setLastName('');
|
} catch (err) {
|
||||||
setEmail('');
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to update user';
|
||||||
} catch (_) {
|
toast.error(msg);
|
||||||
setError('Failed to update user');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleActivation = async (id) => {
|
// --- حذف کاربر ---
|
||||||
await usersAPI.toggleActivation(id);
|
const onDelete = async (id) => {
|
||||||
|
if (!window.confirm('Are you sure you want to delete this user?')) return;
|
||||||
|
try {
|
||||||
|
const result = await usersAPI.remove(id);
|
||||||
await fetchUsers(filter);
|
await fetchUsers(filter);
|
||||||
|
toast.success(result?.Message || 'User deleted successfully');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to delete user';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- تغییر وضعیت فعال بودن ---
|
||||||
|
const onToggleActivation = async (id) => {
|
||||||
|
try {
|
||||||
|
const updated = await usersAPI.toggleActivation(id);
|
||||||
|
await fetchUsers(filter);
|
||||||
|
toast.success(updated?.Message || 'User status updated');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to update status';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ریست پسورد ---
|
||||||
const onResetPassword = async (id) => {
|
const onResetPassword = async (id) => {
|
||||||
await usersAPI.resetPassword(id);
|
try {
|
||||||
alert('Password reset successfully.');
|
const res = await usersAPI.resetPassword(id);
|
||||||
|
toast.success(res?.Message || 'Password reset successfully');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to reset password';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- مدیریت رولها ---
|
||||||
|
const openRolesModal = (user) => {
|
||||||
|
setRolesModalUser(user);
|
||||||
|
setSelectedRoles(user.roles?.map(r => r.id) || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateRoles = async () => {
|
||||||
|
if (!rolesModalUser) return;
|
||||||
|
try {
|
||||||
|
await usersAPI.updateRoles(rolesModalUser.id, selectedRoles);
|
||||||
|
toast.success('Roles updated successfully');
|
||||||
|
setRolesModalUser(null);
|
||||||
|
fetchUsers(filter);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.response?.data?.Message || err?.message || 'Failed to update roles';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ستونهای جدول ---
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
{ key: 'firstName', header: 'First Name' },
|
{ key: 'firstName', header: 'First Name' },
|
||||||
{ key: 'lastName', header: 'Last Name' },
|
{ key: 'lastName', header: 'Last Name' },
|
||||||
{ key: 'email', header: 'Email' },
|
{ key: 'email', header: 'Email' },
|
||||||
{ key: 'isActive', header: 'Active', render: (val) => val ? '✅' : '❌' },
|
{
|
||||||
|
key: 'roles',
|
||||||
|
header: 'Roles',
|
||||||
|
render: (_val, row) => (
|
||||||
|
<span>
|
||||||
|
{row.roles?.length
|
||||||
|
? row.roles.map(r => r.name).join(', ')
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'isActive', header: 'Active', render: (val) => (val ? '✅' : '❌') },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
render: (_val, row) => (
|
render: (_val, row) => (
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button onClick={() => openEdit(row)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-blue-100 text-blue-700 hover:opacity-90">
|
||||||
onClick={() => openEdit(row)}
|
|
||||||
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
|
|
||||||
>
|
|
||||||
<Pencil className="h-4 w-4 mr-1" /> Edit
|
<Pencil className="h-4 w-4 mr-1" /> Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => onDelete(row.id)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-red-100 text-red-700 hover:opacity-90">
|
||||||
onClick={() => onDelete(row.id)}
|
|
||||||
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-red-100 text-red-700 hover:opacity-90"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-1" /> Delete
|
<Trash2 className="h-4 w-4 mr-1" /> Delete
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => onToggleActivation(row.id)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-gray-100 text-gray-700 hover:opacity-90">
|
||||||
onClick={() => onToggleActivation(row.id)}
|
|
||||||
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-gray-100 text-gray-700 hover:opacity-90"
|
|
||||||
>
|
|
||||||
<ShieldOff className="h-4 w-4 mr-1" /> Toggle
|
<ShieldOff className="h-4 w-4 mr-1" /> Toggle
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => onResetPassword(row.id)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-green-100 text-green-700 hover:opacity-90">
|
||||||
onClick={() => onResetPassword(row.id)}
|
|
||||||
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-green-100 text-green-700 hover:opacity-90"
|
|
||||||
>
|
|
||||||
<RefreshCcw className="h-4 w-4 mr-1" /> Reset
|
<RefreshCcw className="h-4 w-4 mr-1" /> Reset
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={() => openRolesModal(row)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-purple-100 text-purple-700 hover:opacity-90">
|
||||||
|
Manage Roles
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
], []);
|
], [filter]);
|
||||||
|
|
||||||
|
const handleFilterChange = useMemo(() => debounce(async (v) => fetchUsers(v), 400), [fetchUsers]);
|
||||||
|
|
||||||
|
// --- مودال اضافه و ویرایش ---
|
||||||
|
const renderModal = (isOpen, title, onSubmit, formState, setFormState, onClose) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={onClose} />
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={onSubmit}>
|
||||||
|
{['firstName', 'lastName', 'email'].map((key) => (
|
||||||
|
<input
|
||||||
|
key={key}
|
||||||
|
value={formState[key]}
|
||||||
|
onChange={(e) => setFormState({ ...formState, [key]: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder={key.replace(/^\w/, (c) => c.toUpperCase())}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="flex justify-end gap-x-2">
|
||||||
|
<button type="button" onClick={onClose} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||||
|
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">{title.includes('Edit') ? 'Update' : 'Add'}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- مودال مدیریت رولها ---
|
||||||
|
const renderRolesModal = () => {
|
||||||
|
if (!rolesModalUser) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setRolesModalUser(null)} />
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Manage Roles for {rolesModalUser.firstName}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 max-h-80 overflow-y-auto">
|
||||||
|
{roles.map(role => (
|
||||||
|
<label key={role.id} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={role.id}
|
||||||
|
checked={selectedRoles.includes(role.id)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setSelectedRoles(prev => checked ? [...prev, role.id] : prev.filter(id => id !== role.id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{role.name}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-x-2 mt-4">
|
||||||
|
<button onClick={() => setRolesModalUser(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||||
|
<button onClick={onUpdateRoles} className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
|
<ToastContainer position="top-right" autoClose={3000} />
|
||||||
|
|
||||||
<div className="mb-6 flex items-start justify-between">
|
<div className="mb-6 flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
Manage users: add, edit, activate/deactivate and reset password
|
Manage users: add, edit, activate/deactivate, reset password, and assign roles
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button onClick={() => setIsModalOpen(true)} className="btn-primary inline-flex items-center">
|
||||||
onClick={() => setIsModalOpen(true)}
|
|
||||||
className="btn-primary inline-flex items-center"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" /> Add User
|
<Plus className="h-4 w-4 mr-2" /> Add User
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Modal */}
|
{renderModal(isModalOpen, 'Add User', onAddUser, addForm, setAddForm, () => setIsModalOpen(false))}
|
||||||
{isModalOpen && (
|
{renderModal(isEditModalOpen, 'Edit User', onUpdateUser, editForm, setEditForm, () => setIsEditModalOpen(false))}
|
||||||
<>
|
{renderRolesModal()}
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md card p-6 relative">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Add User</h3>
|
|
||||||
<form className="space-y-4" onSubmit={onAddUser}>
|
|
||||||
<input
|
|
||||||
value={firstName}
|
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="First Name"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
value={lastName}
|
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="Last Name"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="Email"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-x-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsModalOpen(false)}
|
|
||||||
className="px-4 py-2 border rounded-lg"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Edit Modal */}
|
|
||||||
{isEditModalOpen && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
|
||||||
onClick={() => setIsEditModalOpen(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div className="w-full max-w-md card p-6 relative">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Edit User</h3>
|
|
||||||
<form className="space-y-4" onSubmit={onUpdateUser}>
|
|
||||||
<input
|
|
||||||
value={firstName}
|
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="First Name"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
value={lastName}
|
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="Last Name"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg"
|
|
||||||
placeholder="Email"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-x-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsEditModalOpen(false)}
|
|
||||||
className="px-4 py-2 border rounded-lg"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filter */}
|
|
||||||
<div className="card p-4 mb-4">
|
<div className="card p-4 mb-4">
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={async (e) => {
|
onChange={(e) => {
|
||||||
const v = e.target.value;
|
setFilter(e.target.value);
|
||||||
setFilter(v);
|
handleFilterChange(e.target.value);
|
||||||
await fetchUsers(v);
|
|
||||||
}}
|
}}
|
||||||
placeholder="Filter by name or email"
|
placeholder="Filter by name, email or role"
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
{loading ? (
|
||||||
data={users}
|
<div className="flex justify-center py-10">
|
||||||
columns={columns}
|
<div className="w-8 h-8 border-4 border-t-transparent border-primary-500 rounded-full animate-spin" />
|
||||||
loading={loading}
|
</div>
|
||||||
searchable={false}
|
) : (
|
||||||
/>
|
<DataTable data={users} columns={columns} searchable={false} />
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="mt-4 text-sm text-red-600">{error}</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,145 +1,131 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir";
|
// -----------------------------
|
||||||
|
// تنظیم BASE_URL
|
||||||
|
// -----------------------------
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
|
||||||
|
(import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir");
|
||||||
|
|
||||||
// ساخت instance از axios
|
// -----------------------------
|
||||||
|
// ایجاد instance از axios
|
||||||
|
// -----------------------------
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
withCredentials: true, // ارسال و دریافت cookie/session
|
withCredentials: true,
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// Interceptor پاسخها
|
// Global error handler برای suppress کردن 401/404
|
||||||
|
// -----------------------------
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const originalConsoleError = console.error;
|
||||||
|
const originalConsoleWarn = console.warn;
|
||||||
|
|
||||||
|
console.error = function(...args) {
|
||||||
|
const errorStr = JSON.stringify(args);
|
||||||
|
const isSilentError = errorStr.includes('401') || errorStr.includes('404') ||
|
||||||
|
args.some(arg => typeof arg === 'object' && arg?.response?.status && [401,404].includes(arg.response.status));
|
||||||
|
|
||||||
|
if (isSilentError) return;
|
||||||
|
originalConsoleError.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = function(...args) {
|
||||||
|
const warnStr = JSON.stringify(args);
|
||||||
|
if (warnStr.includes('401') || warnStr.includes('404')) return;
|
||||||
|
originalConsoleWarn.apply(console, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Request interceptor
|
||||||
|
// -----------------------------
|
||||||
|
api.interceptors.request.use(config => {
|
||||||
|
const skipAuthRedirect = config?.skipAuthRedirect === true;
|
||||||
|
if (skipAuthRedirect) {
|
||||||
|
const authState = useAuthStore.getState();
|
||||||
|
if (!authState?.isLoggedIn) {
|
||||||
|
const CancelToken = axios.CancelToken;
|
||||||
|
const source = CancelToken.source();
|
||||||
|
source.cancel('User not logged in');
|
||||||
|
config.cancelToken = source.token;
|
||||||
|
config._skipRequest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, error => Promise.reject(error));
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Response interceptor
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
response => response,
|
||||||
(error) => {
|
error => {
|
||||||
|
if (error?.isSilent) return Promise.reject(error);
|
||||||
|
if (axios.isCancel(error)) return Promise.reject({ isSilent: true, isCancel: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config || {} });
|
||||||
|
|
||||||
const skipRedirect = error?.config?.skipAuthRedirect === true;
|
const skipRedirect = error?.config?.skipAuthRedirect === true;
|
||||||
if (error.response?.status === 401 && !skipRedirect) {
|
const status = error?.response?.status;
|
||||||
// session منقضی شده → هدایت به login
|
|
||||||
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
if (status === 401) {
|
||||||
setLoggedIn(false);
|
if (!skipRedirect) {
|
||||||
|
useAuthStore.getState().setLoggedIn(false);
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
return Promise.reject(error);
|
||||||
|
} else {
|
||||||
|
return Promise.reject({ isSilent: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404 && skipRedirect) {
|
||||||
|
return Promise.reject({ isSilent: true, response: { status: 404, data: { message: 'Not Found' } }, config: error.config });
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(error.response?.data || error);
|
return Promise.reject(error.response?.data || error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// توابع API
|
// Auth API
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
// Login
|
|
||||||
export async function login(username, password) {
|
export async function login(username, password) {
|
||||||
try {
|
const res = await api.post("/api/v1/Auth/SignIn", { userName: username, username, email: username, password });
|
||||||
const res = await api.post("/api/v1/Auth/SignIn", {
|
|
||||||
// include common variants to satisfy different backends
|
|
||||||
userName: username,
|
|
||||||
username: username,
|
|
||||||
email: username,
|
|
||||||
password: password,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// خروج از سیستم
|
|
||||||
export async function signOut() {
|
export async function signOut() {
|
||||||
try {
|
try {
|
||||||
const res = await api.post("/api/v1/Auth/SignOut");
|
await api.post("/api/v1/Auth/SignOut", null, { skipAuthRedirect: true });
|
||||||
// پاک کردن وضعیت login در Zustand
|
|
||||||
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
|
||||||
setLoggedIn(false);
|
|
||||||
window.location.href = "/";
|
|
||||||
return res.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
console.warn("SignOut API error:", error);
|
||||||
}
|
}
|
||||||
|
useAuthStore.getState().setLoggedIn(false);
|
||||||
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
// فراموشی رمز عبور
|
|
||||||
export async function forgotPassword(email) {
|
export async function forgotPassword(email) {
|
||||||
try {
|
|
||||||
const res = await api.post("/api/v1/Auth/ForgotPassword", { email });
|
const res = await api.post("/api/v1/Auth/ForgotPassword", { email });
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// گرفتن دادههای محافظتشده
|
|
||||||
export async function fetchProtectedData(endpoint) {
|
export async function fetchProtectedData(endpoint) {
|
||||||
try {
|
|
||||||
const res = await api.get(endpoint);
|
const res = await api.get(endpoint);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Payments API (mock data)
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Mock data for development
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
stats: {
|
stats: { total: 1247, success: 1189, failed: 58 },
|
||||||
total: 1247,
|
|
||||||
success: 1189,
|
|
||||||
failed: 58
|
|
||||||
},
|
|
||||||
payments: [
|
payments: [
|
||||||
{
|
{ id: 'TXN-001', user: 'John Doe', amount: 299.99, status: 'success', date: '2024-01-15T10:30:00Z', currency: 'USD' },
|
||||||
id: 'TXN-001',
|
{ id: 'TXN-002', user: 'Jane Smith', amount: 150.00, status: 'pending', date: '2024-01-15T11:45:00Z', currency: 'USD' },
|
||||||
user: 'John Doe',
|
{ id: 'TXN-003', user: 'Bob Johnson', amount: 75.50, status: 'failed', date: '2024-01-15T12:15:00Z', currency: 'USD' },
|
||||||
amount: 299.99,
|
{ id: 'TXN-004', user: 'Alice Brown', amount: 450.00, status: 'success', date: '2024-01-15T13:20:00Z', currency: 'USD' },
|
||||||
status: 'success',
|
{ id: 'TXN-005', user: 'Charlie Wilson', amount: 89.99, status: 'success', date: '2024-01-15T14:30:00Z', currency: 'USD' }
|
||||||
date: '2024-01-15T10:30:00Z',
|
|
||||||
currency: 'USD'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'TXN-002',
|
|
||||||
user: 'Jane Smith',
|
|
||||||
amount: 150.00,
|
|
||||||
status: 'pending',
|
|
||||||
date: '2024-01-15T11:45:00Z',
|
|
||||||
currency: 'USD'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'TXN-003',
|
|
||||||
user: 'Bob Johnson',
|
|
||||||
amount: 75.50,
|
|
||||||
status: 'failed',
|
|
||||||
date: '2024-01-15T12:15:00Z',
|
|
||||||
currency: 'USD'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'TXN-004',
|
|
||||||
user: 'Alice Brown',
|
|
||||||
amount: 450.00,
|
|
||||||
status: 'success',
|
|
||||||
date: '2024-01-15T13:20:00Z',
|
|
||||||
currency: 'USD'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'TXN-005',
|
|
||||||
user: 'Charlie Wilson',
|
|
||||||
amount: 89.99,
|
|
||||||
status: 'success',
|
|
||||||
date: '2024-01-15T14:30:00Z',
|
|
||||||
currency: 'USD'
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
chartData: [
|
chartData: [
|
||||||
{ date: '2024-01-09', amount: 1200 },
|
{ date: '2024-01-09', amount: 1200 },
|
||||||
@@ -152,382 +138,117 @@ const mockData = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose mock payment API for dashboard until real endpoints are integrated
|
|
||||||
export const paymentsAPI = {
|
export const paymentsAPI = {
|
||||||
async getStats() {
|
async getStats() { return mockData.stats; },
|
||||||
return mockData.stats;
|
async getChartData() { return mockData.chartData; },
|
||||||
},
|
|
||||||
async getChartData() {
|
|
||||||
return mockData.chartData;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// Roles API (localStorage mock)
|
// Roles API
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
||||||
|
function readRolesFromStorage() { try { const raw = localStorage.getItem(ROLES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }
|
||||||
function readRolesFromStorage() {
|
function writeRolesToStorage(roles) { localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); }
|
||||||
try {
|
function ensureSeedRoles() { if (readRolesFromStorage().length === 0) writeRolesToStorage([
|
||||||
const raw = localStorage.getItem(ROLES_STORAGE_KEY);
|
|
||||||
if (!raw) return [];
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
if (!Array.isArray(parsed)) return [];
|
|
||||||
return parsed;
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeRolesToStorage(roles) {
|
|
||||||
localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureSeedRoles() {
|
|
||||||
const existing = readRolesFromStorage();
|
|
||||||
if (existing.length === 0) {
|
|
||||||
const seed = [
|
|
||||||
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' },
|
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' },
|
||||||
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' },
|
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' },
|
||||||
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
||||||
];
|
]); }
|
||||||
writeRolesToStorage(seed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rolesAPI = {
|
export const rolesAPI = {
|
||||||
async list(queryOrOptions='') {
|
async list(queryOrOptions='') {
|
||||||
// Support both: list('admin') and list({ nameQuery, currentPage, pageSize })
|
const opts = typeof queryOrOptions === 'string' ? { nameQuery: queryOrOptions, currentPage:1, pageSize:100 } : { nameQuery: queryOrOptions?.nameQuery||'', currentPage: queryOrOptions?.currentPage||1, pageSize: queryOrOptions?.pageSize||100 };
|
||||||
const opts = typeof queryOrOptions === 'string'
|
if (!useAuthStore.getState()?.isLoggedIn) { ensureSeedRoles(); return readRolesFromStorage().filter(r=>r.name.toLowerCase().includes((opts.nameQuery||'').toLowerCase())); }
|
||||||
? { nameQuery: queryOrOptions, currentPage: 1, pageSize: 100 }
|
|
||||||
: {
|
|
||||||
nameQuery: queryOrOptions?.nameQuery || '',
|
|
||||||
currentPage: queryOrOptions?.currentPage ?? 1,
|
|
||||||
pageSize: queryOrOptions?.pageSize ?? 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/api/v1/Role', {
|
const res = await api.get('/api/v1/Role', { params: opts, skipAuthRedirect: true });
|
||||||
params: {
|
const items = Array.isArray(res?.data?.data?.data) ? res.data.data.data : [];
|
||||||
nameQuery: opts.nameQuery,
|
|
||||||
currentPage: opts.currentPage,
|
|
||||||
pageSize: opts.pageSize,
|
|
||||||
},
|
|
||||||
// prevent global 401 redirect for optional fetch
|
|
||||||
skipAuthRedirect: true,
|
|
||||||
});
|
|
||||||
const raw = res?.data;
|
|
||||||
// Backend shape example:
|
|
||||||
// { data: { filterSummary: {...}, data: Role[] }, isSuccess: true, ... }
|
|
||||||
const apiItems = Array.isArray(raw?.data?.data) ? raw.data.data : undefined;
|
|
||||||
// Fallbacks for other shapes we may encounter
|
|
||||||
const items = apiItems
|
|
||||||
|| (Array.isArray(raw) ? raw : undefined)
|
|
||||||
|| (Array.isArray(raw?.items) ? raw.items : [])
|
|
||||||
|| [];
|
|
||||||
// Sync a snapshot to local storage for offline UX
|
|
||||||
writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r })));
|
writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r })));
|
||||||
return items;
|
return items;
|
||||||
} catch (_) {
|
} catch { ensureSeedRoles(); return readRolesFromStorage(); }
|
||||||
// Fallback to local filtering
|
|
||||||
ensureSeedRoles();
|
|
||||||
const roles = readRolesFromStorage();
|
|
||||||
const trimmed = String(opts.nameQuery || '').toLowerCase();
|
|
||||||
if (!trimmed) return roles;
|
|
||||||
return roles.filter(r => r.name?.toLowerCase().includes(trimmed));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async create(role) {
|
async create(role) {
|
||||||
const payload = {
|
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[] };
|
||||||
name: String(role?.name || '').trim(),
|
let created = payload;
|
||||||
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
|
if (useAuthStore.getState()?.isLoggedIn) {
|
||||||
// userType: String(role?.userType || '').trim(),
|
try { const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect:true }); created = res?.data||payload; } catch {}
|
||||||
};
|
|
||||||
let created = null;
|
|
||||||
try {
|
|
||||||
// Try real backend
|
|
||||||
const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect: true });
|
|
||||||
created = res?.data || payload;
|
|
||||||
} catch (_) {
|
|
||||||
// Fallback to local-only if backend unavailable
|
|
||||||
created = payload;
|
|
||||||
}
|
}
|
||||||
|
const roles = readRolesFromStorage(); const newRole = { id: crypto.randomUUID(), ...created }; roles.push(newRole); writeRolesToStorage(roles); return newRole;
|
||||||
// Sync into local storage list so UI updates immediately
|
|
||||||
const roles = readRolesFromStorage();
|
|
||||||
const newRole = { id: crypto.randomUUID(), ...created };
|
|
||||||
roles.push(newRole);
|
|
||||||
writeRolesToStorage(roles);
|
|
||||||
return newRole;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async remove(id) {
|
async remove(id) {
|
||||||
try {
|
if (useAuthStore.getState()?.isLoggedIn) { try { await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect:true }); } catch {} }
|
||||||
await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect: true });
|
const roles = readRolesFromStorage(); writeRolesToStorage(roles.filter(r=>r.id!==id)); return { success:true };
|
||||||
} catch (_) {
|
|
||||||
// ignore backend failure, proceed with local removal for UX
|
|
||||||
}
|
|
||||||
const roles = readRolesFromStorage();
|
|
||||||
const next = roles.filter(r => r.id !== id);
|
|
||||||
writeRolesToStorage(next);
|
|
||||||
return { success: true };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async update(id, role) {
|
async update(id, role) {
|
||||||
const existingLocal = readRolesFromStorage().find(r => r.id === id) || {};
|
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[], userType: role?.userType||undefined };
|
||||||
const payload = {
|
if (useAuthStore.getState()?.isLoggedIn) { try { await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect:true }); } catch {} }
|
||||||
name: String(role?.name || '').trim(),
|
const roles = readRolesFromStorage(); const idx = roles.findIndex(r=>r.id===id); if(idx!==-1){ roles[idx]={...roles[idx], ...payload}; writeRolesToStorage(roles); return roles[idx]; }
|
||||||
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
|
const updated={id,...payload}; roles.push(updated); writeRolesToStorage(roles); return updated;
|
||||||
...(role?.userType !== undefined
|
|
||||||
? { userType: String(role?.userType || '').trim() }
|
|
||||||
: (existingLocal.userType ? { userType: existingLocal.userType } : {})),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true });
|
|
||||||
} catch (err) {
|
|
||||||
const status = err?.statusCode || err?.status || err?.response?.status;
|
|
||||||
if (status === 404 || status === 405 || status === 400) {
|
|
||||||
try {
|
|
||||||
await api.put('/api/v1/Role', { id, ...payload }, { skipAuthRedirect: true });
|
|
||||||
} catch (e2) {
|
|
||||||
const s2 = e2?.statusCode || e2?.status || e2?.response?.status;
|
|
||||||
if (s2 === 401 || s2 === 403) {
|
|
||||||
throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status: s2 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (status === 401 || status === 403) {
|
|
||||||
throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = readRolesFromStorage();
|
|
||||||
const idx = roles.findIndex(r => r.id === id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
roles[idx] = { ...roles[idx], ...payload };
|
|
||||||
writeRolesToStorage(roles);
|
|
||||||
return roles[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found locally, append
|
|
||||||
const updated = { id, ...payload };
|
|
||||||
roles.push(updated);
|
|
||||||
writeRolesToStorage(roles);
|
|
||||||
return updated;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// ✅ Users API (React version)
|
// Users API با رولها
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
const USERS_STORAGE_KEY = 'app_users_v1';
|
const USERS_STORAGE_KEY = 'app_users_v1';
|
||||||
|
function readUsersFromStorage(){ try{ const raw = localStorage.getItem(USERS_STORAGE_KEY); return raw?JSON.parse(raw):[]; } catch{return [];} }
|
||||||
// 🧩 localStorage helpers
|
function writeUsersToStorage(users){ localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); }
|
||||||
function readUsersFromStorage() {
|
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem(USERS_STORAGE_KEY);
|
|
||||||
if (!raw) return [];
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return Array.isArray(parsed) ? parsed : [];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeUsersToStorage(users) {
|
|
||||||
localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usersAPI = {
|
export const usersAPI = {
|
||||||
// 📄 لیست کاربران
|
|
||||||
async list({searchQuery='',currentPage=1,pageSize=100}={}) {
|
async list({searchQuery='',currentPage=1,pageSize=100}={}) {
|
||||||
try {
|
const res = await api.get('/api/v1/User',{ params:{searchQuery,currentPage,pageSize}, skipAuthRedirect:true });
|
||||||
const res = await api.get('/api/v1/User', {
|
return res?.data?.data?.data||[];
|
||||||
params: { searchQuery, currentPage, pageSize },
|
|
||||||
skipAuthRedirect: true,
|
|
||||||
});
|
|
||||||
const apiItems = Array.isArray(res?.data?.data?.data)
|
|
||||||
? res.data.data.data
|
|
||||||
: [];
|
|
||||||
writeUsersToStorage(apiItems);
|
|
||||||
return apiItems;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('List users fallback to localStorage');
|
|
||||||
const users = readUsersFromStorage();
|
|
||||||
const trimmed = searchQuery.toLowerCase();
|
|
||||||
return trimmed
|
|
||||||
? users.filter(
|
|
||||||
(u) =>
|
|
||||||
u.firstName?.toLowerCase().includes(trimmed) ||
|
|
||||||
u.lastName?.toLowerCase().includes(trimmed) ||
|
|
||||||
u.email?.toLowerCase().includes(trimmed)
|
|
||||||
)
|
|
||||||
: users;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ➕ افزودن کاربر جدید
|
|
||||||
async create(user){
|
async create(user){
|
||||||
const payload = {
|
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||||
firstName: String(user?.firstName || '').trim(),
|
|
||||||
lastName: String(user?.lastName || '').trim(),
|
|
||||||
email: String(user?.email || '').trim(),
|
|
||||||
mobile: String(user?.mobile || '').trim(),
|
|
||||||
isActive: !!user?.isActive,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true});
|
const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true});
|
||||||
const created = res?.data?.data || payload;
|
|
||||||
|
|
||||||
const users = readUsersFromStorage();
|
|
||||||
const newUser = { id: created.id || crypto.randomUUID(), ...created };
|
|
||||||
users.push(newUser);
|
|
||||||
writeUsersToStorage(users);
|
|
||||||
return newUser;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Create user failed', err);
|
|
||||||
const fallbackUser = { id: crypto.randomUUID(), ...payload };
|
|
||||||
const users = readUsersFromStorage();
|
|
||||||
users.push(fallbackUser);
|
|
||||||
writeUsersToStorage(users);
|
|
||||||
return fallbackUser;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ✏️ ویرایش کاربر
|
|
||||||
async update(id, user) {
|
|
||||||
const payload = {
|
|
||||||
firstName: String(user?.firstName || '').trim(),
|
|
||||||
lastName: String(user?.lastName || '').trim(),
|
|
||||||
email: String(user?.email || '').trim(),
|
|
||||||
mobile: String(user?.mobile || '').trim(),
|
|
||||||
isActive: !!user?.isActive,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.put(`/api/v1/User/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true });
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Update user failed', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = readUsersFromStorage();
|
|
||||||
const idx = users.findIndex((u) => u.id === id);
|
|
||||||
if (idx !== -1) users[idx] = { ...users[idx], ...payload };
|
|
||||||
else users.push({ id, ...payload });
|
|
||||||
writeUsersToStorage(users);
|
|
||||||
return users[idx] || payload;
|
|
||||||
},
|
|
||||||
|
|
||||||
// ❌ حذف کاربر
|
|
||||||
async remove(id) {
|
|
||||||
try {
|
|
||||||
await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`, {
|
|
||||||
skipAuthRedirect: true,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Delete user failed', err);
|
|
||||||
}
|
|
||||||
const users = readUsersFromStorage().filter((u) => u.id !== id);
|
|
||||||
writeUsersToStorage(users);
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
|
|
||||||
// 🔄 تغییر وضعیت فعال بودن (PATCH)
|
|
||||||
async toggleActivation(id) {
|
|
||||||
try {
|
|
||||||
const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`, null, {
|
|
||||||
skipAuthRedirect: true,
|
|
||||||
});
|
|
||||||
const updated = res?.data?.data;
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
const users = readUsersFromStorage();
|
|
||||||
const idx = users.findIndex((u) => u.id === id);
|
|
||||||
if (idx !== -1) {
|
|
||||||
users[idx] = { ...users[idx], isActive: updated.isActive };
|
|
||||||
writeUsersToStorage(users);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Toggle activation failed', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 🔐 ریست پسورد (PATCH)
|
|
||||||
async resetPassword(id) {
|
|
||||||
try {
|
|
||||||
const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`, null, {
|
|
||||||
skipAuthRedirect: true,
|
|
||||||
});
|
|
||||||
return res?.data;
|
return res?.data;
|
||||||
|
},
|
||||||
|
async update(id,user){
|
||||||
|
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||||
|
const res = await api.put(`/api/v1/User/${encodeURIComponent(id)}`,payload,{skipAuthRedirect:true});
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
async remove(id){
|
||||||
|
const res = await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`,{skipAuthRedirect:true});
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
async toggleActivation(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||||
|
async resetPassword(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||||
|
async getRoles(userId){ try{ const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{skipAuthRedirect:true}); return res?.data?.data?.data||[]; }catch(err){ console.error(err); return []; } },
|
||||||
|
|
||||||
|
// مدیریت رولها
|
||||||
|
async getRoles(userId){
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`, { skipAuthRedirect: true });
|
||||||
|
return res?.data?.data || []; // ← اصلاح شد
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error('Reset password failed', err);
|
console.error(err);
|
||||||
throw err;
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||||
|
async updateRoles(userId,roleIds=[]){ try{ const res = await api.post(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// Permissions API
|
// Permissions API
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
export async function listPermissions(){
|
export async function listPermissions(){
|
||||||
try {
|
if(!useAuthStore.getState()?.isLoggedIn) return [
|
||||||
const res = await api.get('/api/v1/General/Permission', { skipAuthRedirect: true });
|
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||||
const raw = res?.data;
|
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||||
// Expected shape: { data: Permission[] }
|
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||||
const items = Array.isArray(raw?.data) ? raw.data : [];
|
|
||||||
// Prefer returning structured objects with name and description
|
|
||||||
const objs = items
|
|
||||||
.map((p) => {
|
|
||||||
const name = typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null);
|
|
||||||
if (!name) return null;
|
|
||||||
const description = typeof p?.description === 'string' && p.description
|
|
||||||
? p.description
|
|
||||||
: (typeof p?.displayName === 'string' ? p.displayName : name);
|
|
||||||
return { name, description };
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
if (objs.length > 0) return objs;
|
|
||||||
// Fallback to simple names if server returned unexpected shape
|
|
||||||
const names = items
|
|
||||||
.map((p) => (typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null)))
|
|
||||||
.filter(Boolean);
|
|
||||||
return names.map((n) => ({ name: n, description: n }));
|
|
||||||
} catch (_) {
|
|
||||||
// Fallback to a minimal static list to keep UI usable
|
|
||||||
return [
|
|
||||||
{ name: 'Administrator', description: 'Full access to administrative features' },
|
|
||||||
{ name: 'UserManagement', description: 'Manage users and their profiles' },
|
|
||||||
{ name: 'AddUser', description: 'Create new user accounts' },
|
|
||||||
{ name: 'EditUser', description: 'Edit existing user accounts' },
|
|
||||||
{ name: 'UserPasswordManagement', description: 'Reset or change user passwords' },
|
|
||||||
{ name: 'UserRoleManagement', description: 'Assign or modify user roles' },
|
|
||||||
{ name: 'RoleManagement', description: 'Manage roles and permissions' },
|
|
||||||
{ name: 'AddRole', description: 'Create new roles' },
|
|
||||||
{ name: 'EditRole', description: 'Edit existing roles' },
|
|
||||||
{ name: 'DeleteRole', description: 'Remove roles from the system' },
|
|
||||||
];
|
];
|
||||||
}
|
try{
|
||||||
|
const res = await api.get('/api/v1/General/Permissions',{skipAuthRedirect:true});
|
||||||
|
const items = Array.isArray(res?.data?.data)?res.data.data:[];
|
||||||
|
return items.map(p=>({name:p.name||p.displayName,description:p.description||p.displayName||p.name})).filter(Boolean);
|
||||||
|
}catch{return [
|
||||||
|
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||||
|
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||||
|
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||||
|
];}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
"/api": {
|
||||||
target: 'https://khalijpay-core.qaserver.ir',
|
target: "https://khalijpay-core.qaserver.ir",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
// اجازه ارسال کوکیها
|
||||||
configure: (proxy) => {
|
configure: (proxy) => {
|
||||||
proxy.on('proxyReq', (proxyReq, req) => {
|
proxy.on("proxyReq", (proxyReq, req) => {
|
||||||
// Ensure cookies are forwarded for withCredentials
|
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
if (origin) proxyReq.setHeader('origin', origin);
|
if (origin) {
|
||||||
|
proxyReq.setHeader("origin", origin);
|
||||||
|
}
|
||||||
|
// اضافه کردن این خط برای اطمینان از ارسال کوکیها
|
||||||
|
proxyReq.setHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on("proxyRes", (proxyRes) => {
|
||||||
|
// اطمینان از دریافت کوکی از سرور
|
||||||
|
proxyRes.headers["Access-Control-Allow-Origin"] = "http://localhost:5173";
|
||||||
|
proxyRes.headers["Access-Control-Allow-Credentials"] = "true";
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on("error", (err) => {
|
||||||
|
console.log("Proxy Error:", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user