feat(users): add roles page and add, edit, delete , toggle ,reset password functionality
This commit is contained in:
14
src/App.jsx
14
src/App.jsx
@@ -9,6 +9,7 @@ import Transactions from './pages/Transactions';
|
||||
import Settings from './pages/Settings';
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
import Roles from './pages/Roles';
|
||||
import Users from './pages/Users';
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
@@ -102,6 +103,19 @@ const AppRoutes = () => {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Users/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "payment-admin-panel",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"lucide-react": "^0.263.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"recharts": "^2.5.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vite": "^4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,270 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { usersAPI } from '../services/api';
|
||||
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
||||
|
||||
const Users = () => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [filter, setFilter] = useState('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
|
||||
const fetchUsers = async (q = '') => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const list = await usersAPI.list(q);
|
||||
setUsers(Array.isArray(list) ? list : []);
|
||||
} catch (e) {
|
||||
setError('Failed to load users');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const onAddUser = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!firstName.trim() || !lastName.trim() || !email.trim()) return;
|
||||
try {
|
||||
await usersAPI.create({ firstName, lastName, email });
|
||||
setFirstName('');
|
||||
setLastName('');
|
||||
setEmail('');
|
||||
await fetchUsers(filter);
|
||||
setIsModalOpen(false);
|
||||
} catch (_) {
|
||||
setError('Failed to create user');
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
setEditingUser(user);
|
||||
setFirstName(user.firstName || '');
|
||||
setLastName(user.lastName || '');
|
||||
setEmail(user.email || '');
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const onUpdateUser = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!editingUser) return;
|
||||
try {
|
||||
await usersAPI.update(editingUser.id, { firstName, lastName, email });
|
||||
await fetchUsers(filter);
|
||||
setIsEditModalOpen(false);
|
||||
setEditingUser(null);
|
||||
setFirstName('');
|
||||
setLastName('');
|
||||
setEmail('');
|
||||
} catch (_) {
|
||||
setError('Failed to update user');
|
||||
}
|
||||
};
|
||||
|
||||
const onToggleActivation = async (id) => {
|
||||
await usersAPI.toggleActivation(id);
|
||||
await fetchUsers(filter);
|
||||
};
|
||||
|
||||
const onResetPassword = async (id) => {
|
||||
await usersAPI.resetPassword(id);
|
||||
alert('Password reset successfully.');
|
||||
};
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'firstName', header: 'First Name' },
|
||||
{ key: 'lastName', header: 'Last Name' },
|
||||
{ key: 'email', header: 'Email' },
|
||||
{ key: 'isActive', header: 'Active', render: (val) => val ? '✅' : '❌' },
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Actions',
|
||||
render: (_val, row) => (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<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"
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-1" /> Edit
|
||||
</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"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" /> Delete
|
||||
</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"
|
||||
>
|
||||
<ShieldOff className="h-4 w-4 mr-1" /> Toggle
|
||||
</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"
|
||||
>
|
||||
<RefreshCcw className="h-4 w-4 mr-1" /> Reset
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], []);
|
||||
|
||||
export default function Users() {
|
||||
return (
|
||||
<div>Users</div>
|
||||
)
|
||||
}
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Manage users: add, edit, activate/deactivate and reset password
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="btn-primary inline-flex items-center"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Modal */}
|
||||
{isModalOpen && (
|
||||
<>
|
||||
<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="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
value={filter}
|
||||
onChange={async (e) => {
|
||||
const v = e.target.value;
|
||||
setFilter(v);
|
||||
await fetchUsers(v);
|
||||
}}
|
||||
placeholder="Filter by name or email"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={users}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
searchable={false}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-sm text-red-600">{error}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
|
||||
@@ -318,6 +318,177 @@ export const rolesAPI = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// ✅ Users API (React version)
|
||||
// -----------------------------
|
||||
|
||||
const USERS_STORAGE_KEY = 'app_users_v1';
|
||||
|
||||
// 🧩 localStorage helpers
|
||||
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 = {
|
||||
// 📄 لیست کاربران
|
||||
async list({ searchQuery = '', currentPage = 1, pageSize = 100 } = {}) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/User', {
|
||||
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) {
|
||||
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 {
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error('Reset password failed', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// Permissions API
|
||||
// -----------------------------
|
||||
|
||||
Reference in New Issue
Block a user