'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> formErrors: Record departments: any[] positions: any[] loadingDepts: boolean isEdit: boolean onCancel: () => void submitting: boolean }) { return (
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 &&

{formErrors.firstName}

}
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 &&

{formErrors.lastName}

}
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 &&

{formErrors.email}

}
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 &&

{formErrors.mobile}

}
{formErrors.departmentId &&

{formErrors.departmentId}

}
{formErrors.positionId &&

{formErrors.positionId}

}
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 &&

{formErrors.hireDate}

}
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 &&

{formErrors.baseSalary}

}
) } function HRContent() { // State Management const [employees, setEmployees] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(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(null) // Form Data const [formData, setFormData] = useState({ 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>({}) const [submitting, setSubmitting] = useState(false) // Departments & Positions for dropdowns const [departments, setDepartments] = useState([]) const [positions, setPositions] = useState([]) const [loadingDepts, setLoadingDepts] = useState(false) // Tabs: employees | departments | orgchart const [activeTab, setActiveTab] = useState<'employees' | 'departments' | 'orgchart'>('employees') const [hierarchy, setHierarchy] = useState([]) const [loadingHierarchy, setLoadingHierarchy] = useState(false) // Department CRUD const [showDeptModal, setShowDeptModal] = useState(false) const [editingDept, setEditingDept] = useState(null) const [deptFormData, setDeptFormData] = useState({ name: '', nameAr: '', code: '', parentId: '' as string, description: '' }) const [deptFormErrors, setDeptFormErrors] = useState>({}) const [deptDeleteConfirm, setDeptDeleteConfirm] = useState(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 = {} 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 = {} 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 (
{/* Header */}

إدارة الموارد البشرية

HR Management System

{activeTab === 'employees' && ( )} {activeTab === 'departments' && ( )}
{/* Tabs */}
{/* Stats Cards */}

Total Employees

{total}

Active

{activeEmployees}

Departments

{departments.length}

Total Salary

{totalSalary >= 1000 ? `${(totalSalary / 1000).toFixed(1)}K` : totalSalary.toLocaleString()}

SAR

{/* Employees Tab */} {activeTab === 'employees' && ( <> {/* Filters and Search */}
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" />
{/* Employees Table */}
{loading ? (
) : error ? (

{error}

) : employees.length === 0 ? (

No employees found

) : ( <>
{employees.map((employee) => ( ))}
Employee Contact Department Position Salary Status Actions
{employee.firstName.charAt(0)}{employee.lastName.charAt(0)}

{employee.firstName} {employee.lastName}

{employee.uniqueEmployeeId}

{employee.email}
{employee.mobile}
{employee.department?.nameAr || employee.department?.name || 'N/A'}
{employee.position?.titleAr || employee.position?.title || 'N/A'} {Number(employee.baseSalary ?? (employee as any).basicSalary ?? 0).toLocaleString()} SAR {employee.status === 'ACTIVE' ? : } {employee.status}
{/* Pagination */}

Showing {((currentPage - 1) * pageSize) + 1} to{' '} {Math.min(currentPage * pageSize, total)} of{' '} {total} employees

{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { const page = i + 1 return ( ) })} {totalPages > 5 && ...}
)}
)} {/* Departments Tab */} {activeTab === 'departments' && (
{loadingDepts ? (
) : departments.length === 0 ? (

No departments yet. Add your first department.

) : (
{departments.map((dept) => ( ))}
Name Code Parent Employees Actions
{dept.nameAr || dept.name} {dept.code} {dept.parent?.nameAr || dept.parent?.name || '-'} {dept._count?.employees ?? 0}
{deptDeleteConfirm === dept.id ? ( ) : ( )}
)}
)} {/* Org Chart Tab */} {activeTab === 'orgchart' && (
{loadingHierarchy ? (
) : ( )}
)}
{/* Create Modal */} { setShowCreateModal(false) resetForm() }} title="Create New Employee" size="xl" >
{ setShowCreateModal(false) resetForm() }} submitting={submitting} />
{/* Department Modal */} { setShowDeptModal(false); setEditingDept(null) }} title={editingDept ? 'Edit Department' : 'Add Department'} >
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 &&

{deptFormErrors.name}

}
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" />
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 &&

{deptFormErrors.code}

}