feat(users): add roles page and add, edit, delete , toggle ,reset password functionality

This commit is contained in:
ghazall-ag
2025-11-07 14:24:09 +03:30
parent 06d430d21c
commit fc229cad99
35 changed files with 4836 additions and 858 deletions

View File

@@ -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>
);

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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
// -----------------------------