1228 lines
46 KiB
TypeScript
1228 lines
46 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
AlertTriangle,
|
|
Edit,
|
|
Plus,
|
|
RefreshCw,
|
|
Save,
|
|
Search,
|
|
Trash2,
|
|
UserX,
|
|
X,
|
|
} from "lucide-react";
|
|
import { Card, CardContent } from "../components/ui/Card.tsx";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "../components/ui/Table.tsx";
|
|
import { Button } from "../components/ui/Button.tsx";
|
|
import { Input, PasswordInput, Select } from "../components/ui/Input.tsx";
|
|
import { useEmployees } from "../hooks/useEmployees.ts";
|
|
import { useDepartments } from "../hooks/useDepartments.ts";
|
|
import { useAuth } from "../contexts/authContext.ts";
|
|
import { api } from "../services/api.ts";
|
|
|
|
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,
|
|
// New fields
|
|
phoneNumber: "",
|
|
aadharNumber: "",
|
|
bankAccountNumber: "",
|
|
bankName: "",
|
|
bankIfsc: "",
|
|
// Contractor-specific
|
|
contractorAgreementNumber: "",
|
|
pfNumber: "",
|
|
esicNumber: "",
|
|
});
|
|
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,
|
|
// New fields
|
|
phoneNumber: formData.phoneNumber || null,
|
|
aadharNumber: formData.aadharNumber || null,
|
|
bankAccountNumber: formData.bankAccountNumber || null,
|
|
bankName: formData.bankName || null,
|
|
bankIfsc: formData.bankIfsc || null,
|
|
// Contractor-specific
|
|
contractorAgreementNumber: formData.contractorAgreementNumber || null,
|
|
pfNumber: formData.pfNumber || null,
|
|
esicNumber: formData.esicNumber || null,
|
|
});
|
|
|
|
// Reset form and switch to list
|
|
setFormData({
|
|
username: "",
|
|
password: "",
|
|
confirmPassword: "",
|
|
name: "",
|
|
email: "",
|
|
role: "Employee",
|
|
departmentId: "",
|
|
contractorId: "",
|
|
isActive: true,
|
|
phoneNumber: "",
|
|
aadharNumber: "",
|
|
bankAccountNumber: "",
|
|
bankName: "",
|
|
bankIfsc: "",
|
|
contractorAgreementNumber: "",
|
|
pfNumber: "",
|
|
esicNumber: "",
|
|
});
|
|
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,
|
|
phoneNumber: user.phone_number || "",
|
|
aadharNumber: user.aadhar_number || "",
|
|
bankAccountNumber: user.bank_account_number || "",
|
|
bankName: user.bank_name || "",
|
|
bankIfsc: user.bank_ifsc || "",
|
|
contractorAgreementNumber: user.contractor_agreement_number || "",
|
|
pfNumber: user.pf_number || "",
|
|
esicNumber: user.esic_number || "",
|
|
});
|
|
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,
|
|
// New fields
|
|
phoneNumber: formData.phoneNumber || null,
|
|
aadharNumber: formData.aadharNumber || null,
|
|
bankAccountNumber: formData.bankAccountNumber || null,
|
|
bankName: formData.bankName || null,
|
|
bankIfsc: formData.bankIfsc || null,
|
|
contractorAgreementNumber: formData.contractorAgreementNumber || null,
|
|
pfNumber: formData.pfNumber || null,
|
|
esicNumber: formData.esicNumber || null,
|
|
});
|
|
|
|
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,
|
|
phoneNumber: "",
|
|
aadharNumber: "",
|
|
bankAccountNumber: "",
|
|
bankName: "",
|
|
bankIfsc: "",
|
|
contractorAgreementNumber: "",
|
|
pfNumber: "",
|
|
esicNumber: "",
|
|
});
|
|
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>REPORTS TO</TableHead>
|
|
<TableHead>STATUS</TableHead>
|
|
<TableHead>ACTIONS</TableHead>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredEmployees.map((user) => {
|
|
// Find supervisor for contractors (supervisor in same department)
|
|
const getSupervisorName = () => {
|
|
if (user.role !== "Contractor") return null;
|
|
const supervisor = employees.find(
|
|
(e) =>
|
|
e.role === "Supervisor" &&
|
|
e.department_id === user.department_id,
|
|
);
|
|
return supervisor?.name || null;
|
|
};
|
|
|
|
// Get reports to info based on role
|
|
const getReportsTo = () => {
|
|
if (user.role === "Employee") {
|
|
return user.contractor_name
|
|
? (
|
|
<span className="text-orange-600">
|
|
{user.contractor_name}
|
|
</span>
|
|
)
|
|
: "-";
|
|
}
|
|
if (user.role === "Contractor") {
|
|
const supervisorName = getSupervisorName();
|
|
return supervisorName
|
|
? (
|
|
<span className="text-blue-600">
|
|
{supervisorName}
|
|
</span>
|
|
)
|
|
: "-";
|
|
}
|
|
return "-";
|
|
};
|
|
|
|
return (
|
|
<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>{getReportsTo()}</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
|
|
/>
|
|
<PasswordInput
|
|
label="Password"
|
|
name="password"
|
|
value={formData.password}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
<Input
|
|
label="Full Name"
|
|
name="name"
|
|
value={formData.name}
|
|
onChange={handleInputChange}
|
|
required
|
|
/>
|
|
<PasswordInput
|
|
label="Confirm Password"
|
|
name="confirmPassword"
|
|
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>
|
|
|
|
{/* Personal & Bank Details - for Employee and Contractor */}
|
|
{(formData.role === "Employee" ||
|
|
formData.role === "Contractor") && (
|
|
<>
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Personal Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Phone Number"
|
|
name="phoneNumber"
|
|
value={formData.phoneNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g., 9876543210"
|
|
/>
|
|
<Input
|
|
label="Aadhar Card Number"
|
|
name="aadharNumber"
|
|
value={formData.aadharNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="12-digit Aadhar number"
|
|
maxLength={12}
|
|
/>
|
|
</div>
|
|
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Bank Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Bank Account Number"
|
|
name="bankAccountNumber"
|
|
value={formData.bankAccountNumber}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="Bank Name"
|
|
name="bankName"
|
|
value={formData.bankName}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="IFSC Code"
|
|
name="bankIfsc"
|
|
value={formData.bankIfsc}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g., SBIN0001234"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Contractor-specific fields */}
|
|
{formData.role === "Contractor" && (
|
|
<>
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Contractor Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Contractor Agreement Number"
|
|
name="contractorAgreementNumber"
|
|
value={formData.contractorAgreementNumber}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="PF Number"
|
|
name="pfNumber"
|
|
value={formData.pfNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="Provident Fund number"
|
|
/>
|
|
<Input
|
|
label="ESIC Number"
|
|
name="esicNumber"
|
|
value={formData.esicNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="ESIC registration number"
|
|
/>
|
|
</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>
|
|
|
|
{/* Personal & Bank Details - for Employee and Contractor */}
|
|
{(formData.role === "Employee" ||
|
|
formData.role === "Contractor") && (
|
|
<>
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Personal Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Phone Number"
|
|
name="phoneNumber"
|
|
value={formData.phoneNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g., 9876543210"
|
|
/>
|
|
<Input
|
|
label="Aadhar Card Number"
|
|
name="aadharNumber"
|
|
value={formData.aadharNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="12-digit Aadhar number"
|
|
maxLength={12}
|
|
/>
|
|
</div>
|
|
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Bank Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Bank Account Number"
|
|
name="bankAccountNumber"
|
|
value={formData.bankAccountNumber}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="Bank Name"
|
|
name="bankName"
|
|
value={formData.bankName}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="IFSC Code"
|
|
name="bankIfsc"
|
|
value={formData.bankIfsc}
|
|
onChange={handleInputChange}
|
|
placeholder="e.g., SBIN0001234"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Contractor-specific fields */}
|
|
{formData.role === "Contractor" && (
|
|
<>
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-6">
|
|
Contractor Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-6 mb-8">
|
|
<Input
|
|
label="Contractor Agreement Number"
|
|
name="contractorAgreementNumber"
|
|
value={formData.contractorAgreementNumber}
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Input
|
|
label="PF Number"
|
|
name="pfNumber"
|
|
value={formData.pfNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="Provident Fund number"
|
|
/>
|
|
<Input
|
|
label="ESIC Number"
|
|
name="esicNumber"
|
|
value={formData.esicNumber}
|
|
onChange={handleInputChange}
|
|
placeholder="ESIC registration number"
|
|
/>
|
|
</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>REPORTS TO</TableHead>
|
|
<TableHead>STATUS</TableHead>
|
|
<TableHead>ACTION</TableHead>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{deletableUsers.map((user) => {
|
|
// Find supervisor for contractors (supervisor in same department)
|
|
const getSupervisorName = () => {
|
|
if (user.role !== "Contractor") return null;
|
|
const supervisor = employees.find(
|
|
(e) =>
|
|
e.role === "Supervisor" &&
|
|
e.department_id === user.department_id,
|
|
);
|
|
return supervisor?.name || null;
|
|
};
|
|
|
|
// Get reports to info based on role
|
|
const getReportsTo = () => {
|
|
if (user.role === "Employee") {
|
|
return user.contractor_name
|
|
? (
|
|
<span className="text-orange-600">
|
|
{user.contractor_name}
|
|
</span>
|
|
)
|
|
: "-";
|
|
}
|
|
if (user.role === "Contractor") {
|
|
const supervisorName = getSupervisorName();
|
|
return supervisorName
|
|
? (
|
|
<span className="text-blue-600">
|
|
{supervisorName}
|
|
</span>
|
|
)
|
|
: "-";
|
|
}
|
|
return "-";
|
|
};
|
|
|
|
return (
|
|
<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>{getReportsTo()}</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>
|
|
);
|
|
};
|