271 lines
8.9 KiB
JavaScript
271 lines
8.9 KiB
JavaScript
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>
|
|
),
|
|
},
|
|
], []);
|
|
|
|
return (
|
|
<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;
|