(Feat): Initial Commit

This commit is contained in:
2025-11-27 22:50:08 +00:00
commit 00f9ed128b
79 changed files with 17413 additions and 0 deletions

786
src/pages/UsersPage.tsx Normal file
View File

@@ -0,0 +1,786 @@
import React, { useState, useMemo } from 'react';
import { RefreshCw, Plus, Trash2, Edit, Save, X, Search, AlertTriangle, UserX } from 'lucide-react';
import { Card, CardHeader, CardContent } from '../components/ui/Card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../components/ui/Table';
import { Button } from '../components/ui/Button';
import { Input, Select } from '../components/ui/Input';
import { useEmployees } from '../hooks/useEmployees';
import { useDepartments } from '../hooks/useDepartments';
import { useAuth } from '../contexts/AuthContext';
import { api } from '../services/api';
export const UsersPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'list' | 'add' | 'edit' | 'delete'>('list');
const [filterRole, setFilterRole] = useState('');
const [filterDept, setFilterDept] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const { employees, loading, error, refresh, createEmployee, deleteEmployee, updateEmployee } = useEmployees();
const { departments } = useDepartments();
const { user: currentUser } = useAuth();
// Form state
const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
name: '',
email: '',
role: 'Employee',
departmentId: '',
contractorId: '',
isActive: true,
});
const [formError, setFormError] = useState('');
const [formLoading, setFormLoading] = useState(false);
const [contractors, setContractors] = useState<any[]>([]);
const [editingUserId, setEditingUserId] = useState<number | null>(null);
// Load contractors when role is Employee
React.useEffect(() => {
if (formData.role === 'Employee') {
api.getUsers({ role: 'Contractor' }).then(setContractors).catch(console.error);
}
}, [formData.role]);
// Check if current user can manage users
const canManageUsers = currentUser?.role === 'SuperAdmin' || currentUser?.role === 'Supervisor';
const isSupervisor = currentUser?.role === 'Supervisor';
// Filter departments for supervisors (only show their department)
const filteredDepartments = isSupervisor
? departments.filter(d => d.id === currentUser?.department_id)
: departments;
const roleOptions = [
{ value: '', label: 'All Roles' },
{ value: 'SuperAdmin', label: 'Super Admin' },
{ value: 'Supervisor', label: 'Supervisor' },
{ value: 'Contractor', label: 'Contractor' },
{ value: 'Employee', label: 'Employee' },
];
const deptOptions = isSupervisor
? [{ value: String(currentUser?.department_id), label: filteredDepartments[0]?.name || 'My Department' }]
: [
{ value: '', label: 'All Departments' },
...departments.map(d => ({ value: String(d.id), label: d.name })),
];
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setFormError('');
};
const handleCreateUser = async () => {
// Validation
if (!formData.username || !formData.password || !formData.name || !formData.email) {
setFormError('Please fill in all required fields');
return;
}
if (formData.password !== formData.confirmPassword) {
setFormError('Passwords do not match');
return;
}
if (formData.password.length < 6) {
setFormError('Password must be at least 6 characters');
return;
}
setFormLoading(true);
setFormError('');
try {
await createEmployee({
username: formData.username,
password: formData.password,
name: formData.name,
email: formData.email,
role: formData.role,
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
});
// Reset form and switch to list
setFormData({
username: '',
password: '',
confirmPassword: '',
name: '',
email: '',
role: 'Employee',
departmentId: '',
contractorId: '',
});
setActiveTab('list');
refresh();
} catch (err: any) {
setFormError(err.message || 'Failed to create user');
} finally {
setFormLoading(false);
}
};
const handleDeleteUser = async (id: number, username: string) => {
if (!confirm(`Are you sure you want to delete user "${username}"?`)) return;
try {
await deleteEmployee(id);
refresh();
} catch (err: any) {
alert(err.message || 'Failed to delete user');
}
};
const handleEditUser = (user: any) => {
setFormData({
username: user.username,
password: '',
confirmPassword: '',
name: user.name,
email: user.email,
role: user.role,
departmentId: user.department_id ? String(user.department_id) : '',
contractorId: user.contractor_id ? String(user.contractor_id) : '',
isActive: user.is_active,
});
setEditingUserId(user.id);
setActiveTab('edit');
setFormError('');
};
const handleUpdateUser = async () => {
if (!formData.name || !formData.email) {
setFormError('Please fill in all required fields');
return;
}
setFormLoading(true);
setFormError('');
try {
await updateEmployee(editingUserId!, {
name: formData.name,
email: formData.email,
role: formData.role,
departmentId: formData.departmentId ? parseInt(formData.departmentId) : null,
contractorId: formData.contractorId ? parseInt(formData.contractorId) : null,
isActive: formData.isActive,
});
resetForm();
setActiveTab('list');
refresh();
} catch (err: any) {
setFormError(err.message || 'Failed to update user');
} finally {
setFormLoading(false);
}
};
const resetForm = () => {
setFormData({
username: '',
password: '',
confirmPassword: '',
name: '',
email: '',
role: 'Employee',
departmentId: '',
contractorId: '',
isActive: true,
});
setEditingUserId(null);
setFormError('');
};
// Auto-set filter for supervisors
React.useEffect(() => {
if (isSupervisor && currentUser?.department_id) {
setFilterDept(String(currentUser.department_id));
}
}, [isSupervisor, currentUser?.department_id]);
// Filter employees
const filteredEmployees = employees.filter(emp => {
// Search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesSearch =
emp.name?.toLowerCase().includes(query) ||
emp.username?.toLowerCase().includes(query) ||
emp.email?.toLowerCase().includes(query) ||
emp.role?.toLowerCase().includes(query);
if (!matchesSearch) return false;
}
if (filterRole && emp.role !== filterRole) return false;
// For supervisors, always filter by their department
if (isSupervisor && currentUser?.department_id) {
if (emp.department_id !== currentUser.department_id) return false;
} else if (filterDept && emp.department_id !== parseInt(filterDept)) {
return false;
}
return true;
});
return (
<div className="p-6">
<Card>
<div className="border-b border-gray-200">
<div className="flex space-x-8 px-6">
<button
onClick={() => { setActiveTab('list'); resetForm(); }}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'list'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
User List
</button>
{canManageUsers && (
<>
<button
onClick={() => { setActiveTab('add'); resetForm(); }}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'add'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Add User
</button>
<button
onClick={() => setActiveTab('edit')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'edit'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Edit User
</button>
<button
onClick={() => setActiveTab('delete')}
className={`py-4 px-2 border-b-2 font-medium text-sm ${
activeTab === 'delete'
? 'border-red-500 text-red-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
Delete Users
</button>
</>
)}
</div>
</div>
<CardContent>
{activeTab === 'list' && (
<div>
<div className="flex gap-4 mb-6">
<div className="relative min-w-[300px] flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search users by name, username, email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<Select
options={deptOptions}
className="w-48 flex-shrink-0"
value={filterDept}
onChange={(e) => setFilterDept(e.target.value)}
disabled={isSupervisor}
/>
<Select
options={roleOptions}
className="w-48 flex-shrink-0"
value={filterRole}
onChange={(e) => setFilterRole(e.target.value)}
/>
<Button variant="ghost" onClick={refresh}>
<RefreshCw size={16} className="mr-2" />
Refresh
</Button>
</div>
<div className="mb-4 text-sm text-gray-600">
Total Users: {filteredEmployees.length}
</div>
{error && (
<div className="text-center py-8 text-red-600">
Error: {error}
</div>
)}
{loading ? (
<div className="text-center py-8">Loading...</div>
) : filteredEmployees.length > 0 ? (
<Table>
<TableHeader>
<TableHead>ID</TableHead>
<TableHead>USERNAME</TableHead>
<TableHead>FULL NAME</TableHead>
<TableHead>EMAIL</TableHead>
<TableHead>ROLE</TableHead>
<TableHead>DEPARTMENT</TableHead>
<TableHead>STATUS</TableHead>
<TableHead>ACTIONS</TableHead>
</TableHeader>
<TableBody>
{filteredEmployees.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'SuperAdmin' ? 'bg-purple-100 text-purple-700' :
user.role === 'Supervisor' ? 'bg-blue-100 text-blue-700' :
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
{canManageUsers && (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditUser(user)}
className="text-blue-600 hover:text-blue-800"
title="Edit"
>
<Edit size={14} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteUser(user.id, user.username)}
className="text-red-600 hover:text-red-800"
title="Delete"
>
<Trash2 size={14} />
</Button>
</div>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : !loading && (
<div className="text-center py-8 text-gray-500">
No users found
</div>
)}
</div>
)}
{activeTab === 'add' && (
<div className="max-w-3xl">
{formError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md">
{formError}
</div>
)}
<h3 className="text-lg font-semibold text-gray-800 mb-6">User Information</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Username"
name="username"
value={formData.username}
onChange={handleInputChange}
required
/>
<Input
label="Password"
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
required
/>
<Input
label="Full Name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
<Input
label="Confirm Password"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
required
/>
<div className="col-span-2">
<Input
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
</div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Role & Department</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Select
label="Role"
name="role"
value={formData.role}
onChange={handleInputChange}
required
options={roleOptions.slice(1)}
/>
<Select
label="Department"
name="departmentId"
value={formData.departmentId}
onChange={handleInputChange}
options={deptOptions.slice(1)}
/>
{formData.role === 'Employee' && (
<Select
label="Contractor (for Employees)"
name="contractorId"
value={formData.contractorId}
onChange={handleInputChange}
options={[
{ value: '', label: 'Select Contractor' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
)}
</div>
<div className="flex justify-end gap-4">
<Button
variant="outline"
onClick={() => { setActiveTab('list'); resetForm(); }}
>
Cancel
</Button>
<Button
size="lg"
onClick={handleCreateUser}
disabled={formLoading}
>
{formLoading ? (
'Creating...'
) : (
<>
<Plus size={16} className="mr-2" />
Create User
</>
)}
</Button>
</div>
</div>
)}
{activeTab === 'edit' && (
<div className="max-w-3xl">
{formError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded-md">
{formError}
</div>
)}
{!editingUserId ? (
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Select User to Edit</h3>
<Select
label="Select User"
value=""
onChange={(e) => {
const user = employees.find(emp => emp.id === parseInt(e.target.value));
if (user) handleEditUser(user);
}}
options={[
{ value: '', label: 'Choose a user to edit...' },
...employees.map(emp => ({
value: String(emp.id),
label: `${emp.name} (${emp.username}) - ${emp.role}`
}))
]}
/>
</div>
) : (
<>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Edit User: {formData.username}</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Input
label="Username"
name="username"
value={formData.username}
disabled
/>
<Input
label="Full Name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
<Input
label="Email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
required
/>
<Select
label="Status"
name="isActive"
value={formData.isActive ? 'true' : 'false'}
onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.value === 'true' }))}
options={[
{ value: 'true', label: 'Active' },
{ value: 'false', label: 'Inactive' },
]}
/>
</div>
<h3 className="text-lg font-semibold text-gray-800 mb-6">Role & Department</h3>
<div className="grid grid-cols-2 gap-6 mb-8">
<Select
label="Role"
name="role"
value={formData.role}
onChange={handleInputChange}
required
options={roleOptions.slice(1)}
/>
<Select
label="Department"
name="departmentId"
value={formData.departmentId}
onChange={handleInputChange}
options={[
{ value: '', label: 'No Department' },
...departments.map(d => ({ value: String(d.id), label: d.name }))
]}
/>
{formData.role === 'Employee' && (
<Select
label="Contractor (for Employees)"
name="contractorId"
value={formData.contractorId}
onChange={handleInputChange}
options={[
{ value: '', label: 'Select Contractor' },
...contractors.map(c => ({ value: String(c.id), label: c.name }))
]}
/>
)}
</div>
<div className="flex justify-end gap-4">
<Button
variant="ghost"
onClick={() => resetForm()}
>
<X size={16} className="mr-2" />
Clear Selection
</Button>
<Button
onClick={handleUpdateUser}
disabled={formLoading}
>
{formLoading ? (
'Saving...'
) : (
<>
<Save size={16} className="mr-2" />
Save Changes
</>
)}
</Button>
</div>
</>
)}
</div>
)}
{activeTab === 'delete' && canManageUsers && (
<div>
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<AlertTriangle className="text-red-500 flex-shrink-0 mt-0.5" size={20} />
<div>
<h4 className="font-semibold text-red-800">Warning: Permanent Action</h4>
<p className="text-sm text-red-700 mt-1">
Deleting a user is permanent and cannot be undone. All associated data will be removed.
{isSupervisor && " As a Supervisor, you can only delete Employees and Contractors in your department."}
</p>
</div>
</div>
</div>
<div className="flex gap-4 mb-6">
<div className="relative min-w-[300px] flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search users to delete..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{!isSupervisor && (
<Select
options={deptOptions}
className="w-48 flex-shrink-0"
value={filterDept}
onChange={(e) => setFilterDept(e.target.value)}
/>
)}
<Select
options={[
{ value: '', label: 'All Deletable Roles' },
{ value: 'Employee', label: 'Employee' },
{ value: 'Contractor', label: 'Contractor' },
]}
className="w-48 flex-shrink-0"
value={filterRole}
onChange={(e) => setFilterRole(e.target.value)}
/>
</div>
{(() => {
// Filter users that can be deleted
const deletableUsers = employees.filter(emp => {
// SuperAdmins and Supervisors cannot be deleted from this tab
if (emp.role === 'SuperAdmin' || emp.role === 'Supervisor') return false;
// Only Employees and Contractors can be deleted
if (emp.role !== 'Employee' && emp.role !== 'Contractor') return false;
// Supervisors can only delete users in their department
if (isSupervisor && emp.department_id !== currentUser?.department_id) return false;
// Apply search filter
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesSearch =
emp.name?.toLowerCase().includes(query) ||
emp.username?.toLowerCase().includes(query) ||
emp.email?.toLowerCase().includes(query);
if (!matchesSearch) return false;
}
// Apply role filter
if (filterRole && emp.role !== filterRole) return false;
// Apply department filter (for SuperAdmin)
if (!isSupervisor && filterDept && emp.department_id !== parseInt(filterDept)) return false;
return true;
});
return (
<>
<div className="mb-4 text-sm text-gray-600">
Deletable Users: {deletableUsers.length}
</div>
{deletableUsers.length > 0 ? (
<Table>
<TableHeader>
<TableHead>ID</TableHead>
<TableHead>USERNAME</TableHead>
<TableHead>FULL NAME</TableHead>
<TableHead>EMAIL</TableHead>
<TableHead>ROLE</TableHead>
<TableHead>DEPARTMENT</TableHead>
<TableHead>STATUS</TableHead>
<TableHead>ACTION</TableHead>
</TableHeader>
<TableBody>
{deletableUsers.map((user) => (
<TableRow key={user.id} className="hover:bg-red-50">
<TableCell>{user.id}</TableCell>
<TableCell className="text-blue-600">{user.username}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'Contractor' ? 'bg-orange-100 text-orange-700' :
'bg-gray-100 text-gray-700'
}`}>
{user.role}
</span>
</TableCell>
<TableCell>{user.department_name || '-'}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.is_active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
onClick={() => {
if (confirm(`Are you sure you want to permanently delete "${user.name}" (${user.username})?\n\nThis action cannot be undone!`)) {
deleteEmployee(user.id);
}
}}
className="text-red-600 border-red-300 hover:bg-red-50 hover:border-red-400"
>
<UserX size={14} className="mr-1" />
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-gray-500">
<UserX size={48} className="mx-auto mb-4 text-gray-300" />
<p>No deletable users found</p>
<p className="text-sm mt-1">
{isSupervisor
? "Only Employees and Contractors in your department can be deleted."
: "Only Employees and Contractors can be deleted from this tab."}
</p>
</div>
)}
</>
);
})()}
</div>
)}
</CardContent>
</Card>
</div>
);
};