1254 lines
48 KiB
TypeScript
1254 lines
48 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback } from 'react'
|
||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||
import Modal from '@/components/Modal'
|
||
import LoadingSpinner from '@/components/LoadingSpinner'
|
||
import Link from 'next/link'
|
||
import { toast } from 'react-hot-toast'
|
||
import {
|
||
Users,
|
||
Plus,
|
||
Search,
|
||
UserPlus,
|
||
Briefcase,
|
||
Calendar,
|
||
DollarSign,
|
||
ArrowLeft,
|
||
Edit,
|
||
Trash2,
|
||
Loader2,
|
||
Mail,
|
||
Phone,
|
||
Building2,
|
||
User,
|
||
CheckCircle2,
|
||
XCircle,
|
||
Network
|
||
} from 'lucide-react'
|
||
import dynamic from 'next/dynamic'
|
||
import { employeesAPI, Employee, CreateEmployeeData, UpdateEmployeeData, EmployeeFilters, departmentsAPI, positionsAPI, type Department } from '@/lib/api/employees'
|
||
|
||
const OrgChart = dynamic(() => import('@/components/hr/OrgChart'), { ssr: false })
|
||
|
||
// Form fields extracted to module level to prevent focus loss on re-render
|
||
function EmployeeFormFields({
|
||
formData,
|
||
setFormData,
|
||
formErrors,
|
||
departments,
|
||
positions,
|
||
loadingDepts,
|
||
isEdit,
|
||
onCancel,
|
||
submitting,
|
||
}: {
|
||
formData: CreateEmployeeData
|
||
setFormData: React.Dispatch<React.SetStateAction<CreateEmployeeData>>
|
||
formErrors: Record<string, string>
|
||
departments: any[]
|
||
positions: any[]
|
||
loadingDepts: boolean
|
||
isEdit: boolean
|
||
onCancel: () => void
|
||
submitting: boolean
|
||
}) {
|
||
return (
|
||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
First Name <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.firstName}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, firstName: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.firstName && <p className="text-red-500 text-xs mt-1">{formErrors.firstName}</p>}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Last Name <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.lastName}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, lastName: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.lastName && <p className="text-red-500 text-xs mt-1">{formErrors.lastName}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Email <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={formData.email}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, email: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.email && <p className="text-red-500 text-xs mt-1">{formErrors.email}</p>}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Mobile <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="tel"
|
||
value={formData.mobile}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, mobile: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.mobile && <p className="text-red-500 text-xs mt-1">{formErrors.mobile}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Department <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.departmentId}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, departmentId: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
disabled={loadingDepts}
|
||
>
|
||
<option value="">Select Department</option>
|
||
{departments.map((dept) => (
|
||
<option key={dept.id} value={dept.id}>{dept.nameAr || dept.name}</option>
|
||
))}
|
||
</select>
|
||
{formErrors.departmentId && <p className="text-red-500 text-xs mt-1">{formErrors.departmentId}</p>}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Position <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.positionId}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, positionId: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
disabled={loadingDepts}
|
||
>
|
||
<option value="">Select Position</option>
|
||
{positions.map((pos) => (
|
||
<option key={pos.id} value={pos.id}>{pos.titleAr || pos.title}</option>
|
||
))}
|
||
</select>
|
||
{formErrors.positionId && <p className="text-red-500 text-xs mt-1">{formErrors.positionId}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Employment Type <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.employmentType}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, employmentType: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
>
|
||
<option value="FULL_TIME">Full Time - دوام كامل</option>
|
||
<option value="PART_TIME">Part Time - دوام جزئي</option>
|
||
<option value="CONTRACT">Contract - عقد</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Contract Type
|
||
</label>
|
||
<select
|
||
value={formData.contractType || ''}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, contractType: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
>
|
||
<option value="UNLIMITED">Unlimited - غير محدود</option>
|
||
<option value="FIXED">Fixed - محدد</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Hire Date <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={formData.hireDate}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, hireDate: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.hireDate && <p className="text-red-500 text-xs mt-1">{formErrors.hireDate}</p>}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Base Salary (SAR) <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.baseSalary}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, baseSalary: parseFloat(e.target.value) || 0 }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{formErrors.baseSalary && <p className="text-red-500 text-xs mt-1">{formErrors.baseSalary}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-end gap-3 pt-4 border-t">
|
||
<button
|
||
type="button"
|
||
onClick={onCancel}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
{isEdit ? 'Updating...' : 'Creating...'}
|
||
</>
|
||
) : (
|
||
<>
|
||
{isEdit ? 'Update Employee' : 'Create Employee'}
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function HRContent() {
|
||
// State Management
|
||
const [employees, setEmployees] = useState<Employee[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
// Pagination
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [totalPages, setTotalPages] = useState(1)
|
||
const [total, setTotal] = useState(0)
|
||
const pageSize = 10
|
||
|
||
// Filters
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [selectedDepartment, setSelectedDepartment] = useState('all')
|
||
const [selectedStatus, setSelectedStatus] = useState('all')
|
||
|
||
// Modals
|
||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||
const [showEditModal, setShowEditModal] = useState(false)
|
||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null)
|
||
|
||
// Form Data
|
||
const [formData, setFormData] = useState<CreateEmployeeData>({
|
||
firstName: '',
|
||
lastName: '',
|
||
firstNameAr: '',
|
||
lastNameAr: '',
|
||
email: '',
|
||
phone: '',
|
||
mobile: '',
|
||
dateOfBirth: '',
|
||
gender: '',
|
||
nationality: 'Saudi Arabia',
|
||
nationalId: '',
|
||
employmentType: 'FULL_TIME',
|
||
contractType: 'UNLIMITED',
|
||
hireDate: '',
|
||
departmentId: '',
|
||
positionId: '',
|
||
reportingToId: '',
|
||
baseSalary: 0
|
||
})
|
||
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
|
||
const [submitting, setSubmitting] = useState(false)
|
||
|
||
// Departments & Positions for dropdowns
|
||
const [departments, setDepartments] = useState<any[]>([])
|
||
const [positions, setPositions] = useState<any[]>([])
|
||
const [loadingDepts, setLoadingDepts] = useState(false)
|
||
|
||
// Tabs: employees | departments | orgchart
|
||
const [activeTab, setActiveTab] = useState<'employees' | 'departments' | 'orgchart'>('employees')
|
||
const [hierarchy, setHierarchy] = useState<Department[]>([])
|
||
const [loadingHierarchy, setLoadingHierarchy] = useState(false)
|
||
|
||
// Department CRUD
|
||
const [showDeptModal, setShowDeptModal] = useState(false)
|
||
const [editingDept, setEditingDept] = useState<Department | null>(null)
|
||
const [deptFormData, setDeptFormData] = useState({ name: '', nameAr: '', code: '', parentId: '' as string, description: '' })
|
||
const [deptFormErrors, setDeptFormErrors] = useState<Record<string, string>>({})
|
||
const [deptDeleteConfirm, setDeptDeleteConfirm] = useState<string | null>(null)
|
||
|
||
const fetchDepartments = useCallback(async () => {
|
||
setLoadingDepts(true)
|
||
try {
|
||
const depts = await departmentsAPI.getAll()
|
||
setDepartments(depts)
|
||
} catch (err) {
|
||
console.error('Failed to load departments:', err)
|
||
} finally {
|
||
setLoadingDepts(false)
|
||
}
|
||
}, [])
|
||
|
||
const fetchHierarchy = useCallback(async () => {
|
||
setLoadingHierarchy(true)
|
||
try {
|
||
const tree = await departmentsAPI.getHierarchy()
|
||
setHierarchy(tree)
|
||
} catch (err) {
|
||
console.error('Failed to load hierarchy:', err)
|
||
} finally {
|
||
setLoadingHierarchy(false)
|
||
}
|
||
}, [])
|
||
|
||
// Fetch Departments & Positions
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
setLoadingDepts(true)
|
||
try {
|
||
const [depts, poss] = await Promise.all([
|
||
departmentsAPI.getAll(),
|
||
positionsAPI.getAll()
|
||
])
|
||
setDepartments(depts)
|
||
setPositions(poss)
|
||
} catch (err) {
|
||
console.error('Failed to load departments/positions:', err)
|
||
} finally {
|
||
setLoadingDepts(false)
|
||
}
|
||
}
|
||
fetchData()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
if (activeTab === 'departments') fetchDepartments()
|
||
if (activeTab === 'orgchart') fetchHierarchy()
|
||
}, [activeTab, fetchDepartments, fetchHierarchy])
|
||
|
||
// Fetch Employees (with debouncing for search)
|
||
const fetchEmployees = useCallback(async () => {
|
||
setLoading(true)
|
||
setError(null)
|
||
try {
|
||
const filters: EmployeeFilters = {
|
||
page: currentPage,
|
||
pageSize,
|
||
}
|
||
|
||
if (searchTerm) filters.search = searchTerm
|
||
if (selectedDepartment !== 'all') filters.departmentId = selectedDepartment
|
||
if (selectedStatus !== 'all') filters.status = selectedStatus
|
||
|
||
const data = await employeesAPI.getAll(filters)
|
||
setEmployees(data.employees)
|
||
setTotal(data.total)
|
||
setTotalPages(data.totalPages)
|
||
} catch (err: any) {
|
||
setError(err.response?.data?.message || 'Failed to load employees')
|
||
toast.error('Failed to load employees')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [currentPage, searchTerm, selectedDepartment, selectedStatus])
|
||
|
||
// Debounced search
|
||
useEffect(() => {
|
||
const debounce = setTimeout(() => {
|
||
setCurrentPage(1)
|
||
fetchEmployees()
|
||
}, 500)
|
||
return () => clearTimeout(debounce)
|
||
}, [searchTerm])
|
||
|
||
// Fetch on filter/page change
|
||
useEffect(() => {
|
||
fetchEmployees()
|
||
}, [currentPage, selectedDepartment, selectedStatus])
|
||
|
||
// Form Validation
|
||
const validateForm = (): boolean => {
|
||
const errors: Record<string, string> = {}
|
||
|
||
if (!formData.firstName || formData.firstName.trim().length < 2) {
|
||
errors.firstName = 'First name must be at least 2 characters'
|
||
}
|
||
|
||
if (!formData.lastName || formData.lastName.trim().length < 2) {
|
||
errors.lastName = 'Last name must be at least 2 characters'
|
||
}
|
||
|
||
if (!formData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||
errors.email = 'Invalid email format'
|
||
}
|
||
|
||
if (!formData.mobile) {
|
||
errors.mobile = 'Mobile is required'
|
||
}
|
||
|
||
if (!formData.departmentId) {
|
||
errors.departmentId = 'Department is required'
|
||
}
|
||
|
||
if (!formData.positionId) {
|
||
errors.positionId = 'Position is required'
|
||
}
|
||
|
||
if (!formData.hireDate) {
|
||
errors.hireDate = 'Hire date is required'
|
||
}
|
||
|
||
if (!formData.baseSalary || formData.baseSalary <= 0) {
|
||
errors.baseSalary = 'Base salary must be greater than 0'
|
||
}
|
||
|
||
setFormErrors(errors)
|
||
return Object.keys(errors).length === 0
|
||
}
|
||
|
||
// Create Employee
|
||
const handleCreate = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (!validateForm()) {
|
||
toast.error('Please fix form errors')
|
||
return
|
||
}
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
await employeesAPI.create(formData)
|
||
toast.success('Employee created successfully!')
|
||
setShowCreateModal(false)
|
||
resetForm()
|
||
fetchEmployees()
|
||
} catch (err: any) {
|
||
const message = err.response?.data?.message || 'Failed to create employee'
|
||
toast.error(message)
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// Edit Employee
|
||
const handleEdit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (!selectedEmployee || !validateForm()) {
|
||
toast.error('Please fix form errors')
|
||
return
|
||
}
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
await employeesAPI.update(selectedEmployee.id, formData as UpdateEmployeeData)
|
||
toast.success('Employee updated successfully!')
|
||
setShowEditModal(false)
|
||
resetForm()
|
||
fetchEmployees()
|
||
} catch (err: any) {
|
||
const message = err.response?.data?.message || 'Failed to update employee'
|
||
toast.error(message)
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// Delete Employee
|
||
const handleDelete = async () => {
|
||
if (!selectedEmployee) return
|
||
|
||
setSubmitting(true)
|
||
try {
|
||
await employeesAPI.delete(selectedEmployee.id)
|
||
toast.success('Employee deleted successfully!')
|
||
setShowDeleteDialog(false)
|
||
setSelectedEmployee(null)
|
||
fetchEmployees()
|
||
} catch (err: any) {
|
||
const message = err.response?.data?.message || 'Failed to delete employee'
|
||
toast.error(message)
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
// Utility Functions
|
||
const resetForm = () => {
|
||
setFormData({
|
||
firstName: '',
|
||
lastName: '',
|
||
firstNameAr: '',
|
||
lastNameAr: '',
|
||
email: '',
|
||
phone: '',
|
||
mobile: '',
|
||
dateOfBirth: '',
|
||
gender: '',
|
||
nationality: 'Saudi Arabia',
|
||
nationalId: '',
|
||
employmentType: 'FULL_TIME',
|
||
contractType: 'UNLIMITED',
|
||
hireDate: '',
|
||
departmentId: '',
|
||
positionId: '',
|
||
reportingToId: '',
|
||
baseSalary: 0
|
||
})
|
||
setFormErrors({})
|
||
setSelectedEmployee(null)
|
||
}
|
||
|
||
const openEditModal = (employee: Employee) => {
|
||
setSelectedEmployee(employee)
|
||
setFormData({
|
||
firstName: employee.firstName,
|
||
lastName: employee.lastName,
|
||
firstNameAr: employee.firstNameAr,
|
||
lastNameAr: employee.lastNameAr,
|
||
email: employee.email,
|
||
phone: employee.phone,
|
||
mobile: employee.mobile,
|
||
dateOfBirth: employee.dateOfBirth?.split('T')[0],
|
||
gender: employee.gender,
|
||
nationality: employee.nationality,
|
||
nationalId: employee.nationalId,
|
||
employmentType: employee.employmentType,
|
||
contractType: employee.contractType,
|
||
hireDate: employee.hireDate?.split('T')[0],
|
||
departmentId: employee.departmentId,
|
||
positionId: employee.positionId,
|
||
reportingToId: employee.reportingToId,
|
||
baseSalary: employee.baseSalary ?? (employee as any).basicSalary ?? 0
|
||
})
|
||
setShowEditModal(true)
|
||
}
|
||
|
||
const openDeleteDialog = (employee: Employee) => {
|
||
setSelectedEmployee(employee)
|
||
setShowDeleteDialog(true)
|
||
}
|
||
|
||
// Department CRUD
|
||
const openDeptModal = (dept?: Department) => {
|
||
if (dept) {
|
||
setEditingDept(dept)
|
||
setDeptFormData({
|
||
name: dept.name,
|
||
nameAr: dept.nameAr || '',
|
||
code: dept.code,
|
||
parentId: dept.parentId || '',
|
||
description: dept.description || ''
|
||
})
|
||
} else {
|
||
setEditingDept(null)
|
||
setDeptFormData({ name: '', nameAr: '', code: '', parentId: '', description: '' })
|
||
}
|
||
setDeptFormErrors({})
|
||
setShowDeptModal(true)
|
||
}
|
||
|
||
const validateDeptForm = () => {
|
||
const err: Record<string, string> = {}
|
||
if (!deptFormData.name?.trim()) err.name = 'Name is required'
|
||
if (!deptFormData.code?.trim()) err.code = 'Code is required'
|
||
setDeptFormErrors(err)
|
||
return Object.keys(err).length === 0
|
||
}
|
||
|
||
const handleSaveDept = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (!validateDeptForm()) return
|
||
try {
|
||
if (editingDept) {
|
||
await departmentsAPI.update(editingDept.id, {
|
||
...deptFormData,
|
||
parentId: deptFormData.parentId || null
|
||
})
|
||
toast.success('Department updated')
|
||
} else {
|
||
await departmentsAPI.create({
|
||
...deptFormData,
|
||
parentId: deptFormData.parentId || undefined
|
||
})
|
||
toast.success('Department created')
|
||
}
|
||
setShowDeptModal(false)
|
||
fetchDepartments()
|
||
if (activeTab === 'orgchart') fetchHierarchy()
|
||
setDepartments(await departmentsAPI.getAll())
|
||
} catch (err: any) {
|
||
toast.error(err.response?.data?.message || 'Failed to save department')
|
||
}
|
||
}
|
||
|
||
const handleDeleteDept = async (id: string) => {
|
||
try {
|
||
await departmentsAPI.delete(id)
|
||
toast.success('Department deleted')
|
||
setDeptDeleteConfirm(null)
|
||
fetchDepartments()
|
||
if (activeTab === 'orgchart') fetchHierarchy()
|
||
setDepartments(await departmentsAPI.getAll())
|
||
} catch (err: any) {
|
||
toast.error(err.response?.data?.message || 'Failed to delete department')
|
||
setDeptDeleteConfirm(null)
|
||
}
|
||
}
|
||
|
||
// Calculate stats (coerce to number - API may return Decimal as string/object)
|
||
const activeEmployees = employees.filter(e => e.status === 'ACTIVE').length
|
||
const totalSalary = employees.reduce((sum, e) => {
|
||
const sal = e.baseSalary ?? (e as any).basicSalary ?? 0
|
||
return sum + Number(sal)
|
||
}, 0)
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Header */}
|
||
<header className="bg-white shadow-sm border-b">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<Link
|
||
href="/dashboard"
|
||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||
>
|
||
<ArrowLeft className="h-5 w-5 text-gray-600" />
|
||
</Link>
|
||
<div className="flex items-center gap-3">
|
||
<div className="bg-red-100 p-2 rounded-lg">
|
||
<Users className="h-6 w-6 text-red-600" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">إدارة الموارد البشرية</h1>
|
||
<p className="text-sm text-gray-600">HR Management System</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
{activeTab === 'employees' && (
|
||
<button
|
||
onClick={() => {
|
||
resetForm()
|
||
setShowCreateModal(true)
|
||
}}
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
Add Employee
|
||
</button>
|
||
)}
|
||
{activeTab === 'departments' && (
|
||
<button
|
||
onClick={() => openDeptModal()}
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
Add Department
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="border-b border-gray-200 bg-white">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<nav className="flex gap-4">
|
||
<button
|
||
onClick={() => setActiveTab('employees')}
|
||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||
activeTab === 'employees'
|
||
? 'border-red-600 text-red-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Users className="h-4 w-4" />
|
||
الموظفون / Employees
|
||
</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('departments')}
|
||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||
activeTab === 'departments'
|
||
? 'border-red-600 text-red-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Building2 className="h-4 w-4" />
|
||
الأقسام / Departments
|
||
</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('orgchart')}
|
||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||
activeTab === 'orgchart'
|
||
? 'border-red-600 text-red-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<Network className="h-4 w-4" />
|
||
الهيكل التنظيمي / Org Chart
|
||
</span>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Total Employees</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">{total}</p>
|
||
</div>
|
||
<div className="bg-blue-100 p-3 rounded-lg">
|
||
<Users className="h-8 w-8 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Active</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">{activeEmployees}</p>
|
||
</div>
|
||
<div className="bg-green-100 p-3 rounded-lg">
|
||
<CheckCircle2 className="h-8 w-8 text-green-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Departments</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">{departments.length}</p>
|
||
</div>
|
||
<div className="bg-purple-100 p-3 rounded-lg">
|
||
<Building2 className="h-8 w-8 text-purple-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">Total Salary</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||
{totalSalary >= 1000
|
||
? `${(totalSalary / 1000).toFixed(1)}K`
|
||
: totalSalary.toLocaleString()}
|
||
</p>
|
||
<p className="text-xs text-gray-600 mt-1">SAR</p>
|
||
</div>
|
||
<div className="bg-orange-100 p-3 rounded-lg">
|
||
<DollarSign className="h-8 w-8 text-orange-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Employees Tab */}
|
||
{activeTab === 'employees' && (
|
||
<>
|
||
{/* Filters and Search */}
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search employees..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full pr-10 pl-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
</div>
|
||
|
||
<select
|
||
value={selectedDepartment}
|
||
onChange={(e) => setSelectedDepartment(e.target.value)}
|
||
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
>
|
||
<option value="all">All Departments</option>
|
||
{departments.map(dept => (
|
||
<option key={dept.id} value={dept.id}>{dept.nameAr || dept.name}</option>
|
||
))}
|
||
</select>
|
||
|
||
<select
|
||
value={selectedStatus}
|
||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
>
|
||
<option value="all">All Status</option>
|
||
<option value="ACTIVE">Active</option>
|
||
<option value="INACTIVE">Inactive</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Employees Table */}
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||
{loading ? (
|
||
<div className="p-12">
|
||
<LoadingSpinner size="lg" message="Loading employees..." />
|
||
</div>
|
||
) : error ? (
|
||
<div className="p-12 text-center">
|
||
<p className="text-red-600 mb-4">{error}</p>
|
||
<button
|
||
onClick={fetchEmployees}
|
||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||
>
|
||
Retry
|
||
</button>
|
||
</div>
|
||
) : employees.length === 0 ? (
|
||
<div className="p-12 text-center">
|
||
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||
<p className="text-gray-600 mb-4">No employees found</p>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||
>
|
||
Create First Employee
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 border-b border-gray-200">
|
||
<tr>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Employee</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Contact</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Department</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Position</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Salary</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Status</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200">
|
||
{employees.map((employee) => (
|
||
<tr key={employee.id} className="hover:bg-gray-50 transition-colors">
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center text-white font-bold">
|
||
{employee.firstName.charAt(0)}{employee.lastName.charAt(0)}
|
||
</div>
|
||
<div>
|
||
<p className="font-semibold text-gray-900">
|
||
{employee.firstName} {employee.lastName}
|
||
</p>
|
||
<p className="text-xs text-gray-600">{employee.uniqueEmployeeId}</p>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="space-y-1">
|
||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||
<Mail className="h-4 w-4" />
|
||
{employee.email}
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||
<Phone className="h-4 w-4" />
|
||
{employee.mobile}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<Building2 className="h-4 w-4 text-gray-400" />
|
||
<span className="text-sm text-gray-900">
|
||
{employee.department?.nameAr || employee.department?.name || 'N/A'}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<span className="text-sm text-gray-900">
|
||
{employee.position?.titleAr || employee.position?.title || 'N/A'}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<span className="text-sm font-semibold text-gray-900">
|
||
{Number(employee.baseSalary ?? (employee as any).basicSalary ?? 0).toLocaleString()} SAR
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||
employee.status === 'ACTIVE'
|
||
? 'bg-green-100 text-green-700'
|
||
: 'bg-gray-100 text-gray-700'
|
||
}`}>
|
||
{employee.status === 'ACTIVE' ? <CheckCircle2 className="h-3 w-3 mr-1" /> : <XCircle className="h-3 w-3 mr-1" />}
|
||
{employee.status}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => openEditModal(employee)}
|
||
className="p-2 hover:bg-green-50 text-green-600 rounded-lg transition-colors"
|
||
title="Edit"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => openDeleteDialog(employee)}
|
||
className="p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors"
|
||
title="Delete"
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||
<p className="text-sm text-gray-600">
|
||
Showing <span className="font-semibold">{((currentPage - 1) * pageSize) + 1}</span> to{' '}
|
||
<span className="font-semibold">{Math.min(currentPage * pageSize, total)}</span> of{' '}
|
||
<span className="font-semibold">{total}</span> employees
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setCurrentPage(currentPage - 1)}
|
||
disabled={currentPage === 1}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Previous
|
||
</button>
|
||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||
const page = i + 1
|
||
return (
|
||
<button
|
||
key={page}
|
||
onClick={() => setCurrentPage(page)}
|
||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
currentPage === page
|
||
? 'bg-red-600 text-white'
|
||
: 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{page}
|
||
</button>
|
||
)
|
||
})}
|
||
{totalPages > 5 && <span className="px-2">...</span>}
|
||
<button
|
||
onClick={() => setCurrentPage(currentPage + 1)}
|
||
disabled={currentPage === totalPages}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Departments Tab */}
|
||
{activeTab === 'departments' && (
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||
{loadingDepts ? (
|
||
<div className="p-12"><LoadingSpinner size="lg" message="Loading departments..." /></div>
|
||
) : departments.length === 0 ? (
|
||
<div className="p-12 text-center">
|
||
<Building2 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||
<p className="text-gray-600 mb-4">No departments yet. Add your first department.</p>
|
||
<button onClick={() => openDeptModal()} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
|
||
Add Department
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 border-b border-gray-200">
|
||
<tr>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Name</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Code</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Parent</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Employees</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200">
|
||
{departments.map((dept) => (
|
||
<tr key={dept.id} className="hover:bg-gray-50">
|
||
<td className="px-6 py-4">
|
||
<span className="font-semibold text-gray-900">{dept.nameAr || dept.name}</span>
|
||
</td>
|
||
<td className="px-6 py-4 text-sm text-gray-600">{dept.code}</td>
|
||
<td className="px-6 py-4 text-sm text-gray-600">{dept.parent?.nameAr || dept.parent?.name || '-'}</td>
|
||
<td className="px-6 py-4 text-sm">{dept._count?.employees ?? 0}</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex gap-2">
|
||
<button onClick={() => openDeptModal(dept)} className="p-2 hover:bg-blue-50 text-blue-600 rounded-lg" title="Edit">
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
{deptDeleteConfirm === dept.id ? (
|
||
<span className="flex gap-1">
|
||
<button onClick={() => handleDeleteDept(dept.id)} className="px-2 py-1 text-xs bg-red-600 text-white rounded">Confirm</button>
|
||
<button onClick={() => setDeptDeleteConfirm(null)} className="px-2 py-1 text-xs border rounded">Cancel</button>
|
||
</span>
|
||
) : (
|
||
<button onClick={() => setDeptDeleteConfirm(dept.id)} className="p-2 hover:bg-red-50 text-red-600 rounded-lg" title="Delete">
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Org Chart Tab */}
|
||
{activeTab === 'orgchart' && (
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||
{loadingHierarchy ? (
|
||
<div className="p-12"><LoadingSpinner size="lg" message="Loading org chart..." /></div>
|
||
) : (
|
||
<OrgChart hierarchy={hierarchy} />
|
||
)}
|
||
</div>
|
||
)}
|
||
</main>
|
||
|
||
{/* Create Modal */}
|
||
<Modal
|
||
isOpen={showCreateModal}
|
||
onClose={() => {
|
||
setShowCreateModal(false)
|
||
resetForm()
|
||
}}
|
||
title="Create New Employee"
|
||
size="xl"
|
||
>
|
||
<form onSubmit={handleCreate}>
|
||
<EmployeeFormFields
|
||
formData={formData}
|
||
setFormData={setFormData}
|
||
formErrors={formErrors}
|
||
departments={departments}
|
||
positions={positions}
|
||
loadingDepts={loadingDepts}
|
||
isEdit={false}
|
||
onCancel={() => {
|
||
setShowCreateModal(false)
|
||
resetForm()
|
||
}}
|
||
submitting={submitting}
|
||
/>
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Department Modal */}
|
||
<Modal
|
||
isOpen={showDeptModal}
|
||
onClose={() => { setShowDeptModal(false); setEditingDept(null) }}
|
||
title={editingDept ? 'Edit Department' : 'Add Department'}
|
||
>
|
||
<form onSubmit={handleSaveDept} className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">Name (EN) *</label>
|
||
<input
|
||
type="text"
|
||
value={deptFormData.name}
|
||
onChange={(e) => setDeptFormData((p) => ({ ...p, name: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||
/>
|
||
{deptFormErrors.name && <p className="text-red-500 text-xs mt-1">{deptFormErrors.name}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">Name (AR)</label>
|
||
<input
|
||
type="text"
|
||
value={deptFormData.nameAr}
|
||
onChange={(e) => setDeptFormData((p) => ({ ...p, nameAr: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">Code *</label>
|
||
<input
|
||
type="text"
|
||
value={deptFormData.code}
|
||
onChange={(e) => setDeptFormData((p) => ({ ...p, code: e.target.value.toUpperCase() }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||
placeholder="e.g. HR, IT, SALES"
|
||
/>
|
||
{deptFormErrors.code && <p className="text-red-500 text-xs mt-1">{deptFormErrors.code}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">Parent Department</label>
|
||
<select
|
||
value={deptFormData.parentId}
|
||
onChange={(e) => setDeptFormData((p) => ({ ...p, parentId: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||
>
|
||
<option value="">— None (Root) —</option>
|
||
{departments
|
||
.filter((d) => !editingDept || d.id !== editingDept.id)
|
||
.map((d) => (
|
||
<option key={d.id} value={d.id}>{d.nameAr || d.name} ({d.code})</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||
<textarea
|
||
value={deptFormData.description}
|
||
onChange={(e) => setDeptFormData((p) => ({ ...p, description: e.target.value }))}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500"
|
||
rows={2}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-end gap-2 pt-4">
|
||
<button type="button" onClick={() => setShowDeptModal(false)} className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
|
||
{editingDept ? 'Update' : 'Create'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Edit Modal */}
|
||
<Modal
|
||
isOpen={showEditModal}
|
||
onClose={() => {
|
||
setShowEditModal(false)
|
||
resetForm()
|
||
}}
|
||
title="Edit Employee"
|
||
size="xl"
|
||
>
|
||
<form onSubmit={handleEdit}>
|
||
<EmployeeFormFields
|
||
formData={formData}
|
||
setFormData={setFormData}
|
||
formErrors={formErrors}
|
||
departments={departments}
|
||
positions={positions}
|
||
loadingDepts={loadingDepts}
|
||
isEdit
|
||
onCancel={() => {
|
||
setShowEditModal(false)
|
||
resetForm()
|
||
}}
|
||
submitting={submitting}
|
||
/>
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
{showDeleteDialog && selectedEmployee && (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteDialog(false)} />
|
||
<div className="flex min-h-screen items-center justify-center p-4">
|
||
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<div className="bg-red-100 p-3 rounded-full">
|
||
<Trash2 className="h-6 w-6 text-red-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold text-gray-900">Delete Employee</h3>
|
||
<p className="text-sm text-gray-600">This action cannot be undone</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-gray-700 mb-6">
|
||
Are you sure you want to delete <span className="font-semibold">{selectedEmployee.firstName} {selectedEmployee.lastName}</span>?
|
||
</p>
|
||
<div className="flex items-center justify-end gap-3">
|
||
<button
|
||
onClick={() => {
|
||
setShowDeleteDialog(false)
|
||
setSelectedEmployee(null)
|
||
}}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
onClick={handleDelete}
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
Deleting...
|
||
</>
|
||
) : (
|
||
'Delete Employee'
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function HRPage() {
|
||
return (
|
||
<ProtectedRoute>
|
||
<HRContent />
|
||
</ProtectedRoute>
|
||
)
|
||
}
|