429 lines
16 KiB
TypeScript
429 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Layout } from '../components/Layout/Layout';
|
|
import { Users, Edit, Trash2, Plus, Search, Filter, Save, X } from 'lucide-react';
|
|
import { User, UserRole } from '../types';
|
|
import { useAuth } from '../context/AuthContext';
|
|
|
|
const API_URL = 'http://localhost:3001/api';
|
|
|
|
export const UserManagement: React.FC = () => {
|
|
const { user: currentUser } = useAuth();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
role: 'employee' as UserRole,
|
|
jobTitle: '',
|
|
department: '',
|
|
phone: '',
|
|
address: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadUsers();
|
|
}, []);
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem('token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
};
|
|
};
|
|
|
|
const loadUsers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`${API_URL}/users`, {
|
|
headers: getAuthHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load users');
|
|
}
|
|
|
|
const data = await response.json();
|
|
// Map API response to User type
|
|
const mappedUsers: User[] = data.map((u: any) => ({
|
|
id: u.id,
|
|
name: u.name,
|
|
email: u.email,
|
|
role: u.role,
|
|
}));
|
|
setUsers(mappedUsers);
|
|
} catch (error) {
|
|
console.error('Error loading users:', error);
|
|
setError('Failed to load users');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
setEditingUser(null);
|
|
setFormData({ name: '', email: '', role: 'employee', jobTitle: '', department: '', phone: '', address: '' });
|
|
setShowForm(true);
|
|
setError('');
|
|
};
|
|
|
|
const handleEdit = (user: User) => {
|
|
setEditingUser(user);
|
|
setFormData({
|
|
name: user.name,
|
|
email: user.email,
|
|
role: user.role,
|
|
jobTitle: '',
|
|
department: '',
|
|
phone: '',
|
|
address: '',
|
|
});
|
|
setShowForm(true);
|
|
setError('');
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!window.confirm('Are you sure you want to delete this user?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetch(`${API_URL}/users/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders(),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to delete user');
|
|
}
|
|
|
|
await loadUsers();
|
|
} catch (error: any) {
|
|
console.error('Error deleting user:', error);
|
|
setError(error.message || 'Failed to delete user');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setLoading(true);
|
|
|
|
try {
|
|
if (editingUser) {
|
|
// Update existing user
|
|
const response = await fetch(`${API_URL}/users/${editingUser.id}`, {
|
|
method: 'PUT',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to update user');
|
|
}
|
|
|
|
await loadUsers();
|
|
setShowForm(false);
|
|
setEditingUser(null);
|
|
} else {
|
|
// Create new user
|
|
const response = await fetch(`${API_URL}/users`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to create user');
|
|
}
|
|
|
|
await loadUsers();
|
|
setShowForm(false);
|
|
setFormData({ name: '', email: '', role: 'employee', jobTitle: '', department: '', phone: '', address: '' });
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error saving user:', error);
|
|
setError(error.message || 'Failed to save user');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filteredUsers = users.filter(u =>
|
|
u.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
u.email.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<Layout>
|
|
<div>
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-3xl font-bold text-gray-900">User & Role Management</h1>
|
|
<button
|
|
onClick={handleCreate}
|
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
New User
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-lg px-4 py-2">
|
|
<Search className="w-4 h-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search users..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="bg-transparent border-none outline-none flex-1"
|
|
/>
|
|
</div>
|
|
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
|
|
<Filter className="w-4 h-4" />
|
|
Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Users Table */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
|
{loading && !users.length ? (
|
|
<div className="p-8 text-center text-gray-500">Loading users...</div>
|
|
) : (
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{filteredUsers.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={4} className="px-6 py-8 text-center text-gray-500">
|
|
No users found
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
filteredUsers.map((user) => (
|
|
<tr key={user.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">{user.name}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{user.email}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
user.role === 'admin' ? 'bg-purple-100 text-purple-800' :
|
|
user.role === 'hr' ? 'bg-blue-100 text-blue-800' :
|
|
user.role === 'payroll' ? 'bg-green-100 text-green-800' :
|
|
user.role === 'manager' ? 'bg-yellow-100 text-yellow-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{user.role.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => handleEdit(user)}
|
|
className="text-primary-600 hover:text-primary-900"
|
|
title="Edit"
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</button>
|
|
{user.id !== currentUser?.id && (
|
|
<button
|
|
onClick={() => handleDelete(user.id)}
|
|
className="text-red-600 hover:text-red-900"
|
|
title="Delete"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
|
|
{/* Create/Edit Form Modal */}
|
|
{showForm && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-xl font-bold text-gray-900">
|
|
{editingUser ? 'Edit User' : 'Create New User'}
|
|
</h2>
|
|
<button
|
|
onClick={() => {
|
|
setShowForm(false);
|
|
setEditingUser(null);
|
|
}}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-4">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Full Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
required
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Email Address
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
required
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Role
|
|
</label>
|
|
<select
|
|
value={formData.role}
|
|
onChange={(e) => setFormData({ ...formData, role: e.target.value as UserRole })}
|
|
required
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
>
|
|
<option value="employee">Employee</option>
|
|
<option value="manager">Manager</option>
|
|
<option value="hr">HR</option>
|
|
<option value="payroll">Payroll</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
|
|
{formData.role !== 'admin' && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Job Title
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.jobTitle}
|
|
onChange={(e) => setFormData({ ...formData, jobTitle: e.target.value })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Department
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.department}
|
|
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Phone
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={formData.phone}
|
|
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Address
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.address}
|
|
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="flex gap-2 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowForm(false);
|
|
setEditingUser(null);
|
|
}}
|
|
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
{loading ? 'Saving...' : (editingUser ? 'Update' : 'Create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
|