'use client'; import React, { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { Search, Plus, MoreHorizontal, X, Pencil, Trash2, AlertTriangle, Upload, Camera, Link as LinkIcon, Clock, MapPin, Briefcase } from 'lucide-react'; import employeeService from '@/services/employeeService'; import apiClient from '@/services/apiClient'; import { useCommonCodes } from '@/hooks/useCommonCodes'; import { useAuthStore } from '@/stores'; import type { Employee, RoleCode, EmpStatus, DispatchStatus, DispatchHistory, Project, EquipmentAssignment } from '@/types'; const statusColor: Record = { ACTIVE: 'bg-status-resolved-bg text-status-resolved-text', ON_LEAVE: 'bg-status-pending-bg text-status-pending-text', RESIGNED: 'bg-status-critical-bg text-status-critical-text', }; const initialColors = ['bg-indigo-500', 'bg-purple-500', 'bg-blue-500', 'bg-green-500', 'bg-orange-500', 'bg-pink-500']; function getInitialColor(name: string, index: number) { return initialColors[index % initialColors.length]; } export default function EmployeesPage() { const [filter, setFilter] = useState('all'); const [employees, setEmployees] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const router = useRouter(); const { user } = useAuthStore(); const { codes: empStatusCodes, loading: statusCodesLoading } = useCommonCodes('EMP_STATUS'); const { codes: empRoleCodes, loading: roleCodesLoading } = useCommonCodes('EMP_ROLE'); const roleLabel = roleCodesLoading ? {} : Object.fromEntries(empRoleCodes.map((c) => [c.codeValue, c.codeName])); // Add Employee Modal State const [isModalOpen, setIsModalOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [modalError, setModalError] = useState(null); const [formData, setFormData] = useState({ loginId: '', name: '', email: '', roleCode: 'EMPLOYEE' as RoleCode, empStatus: 'ACTIVE' as EmpStatus, dispatchStatus: 'WAITING' as DispatchStatus, }); // Edit Modal State const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [editModalError, setEditModalError] = useState(null); const [editingEmployee, setEditingEmployee] = useState(null); // Edit Modal - Section data const [editBasic, setEditBasic] = useState({ name: '', email: '', phone: '', joinedDate: '', birthDate: '', education: '', major: '', graduationDate: '', residence: '', profileImage: '', }); const [editStatus, setEditStatus] = useState({ empStatus: 'ACTIVE' as EmpStatus, leftDate: '', }); const [editRole, setEditRole] = useState('EMPLOYEE'); const [editDispatchStatus, setEditDispatchStatus] = useState('WAITING'); // Edit Modal - Fetched data const [equipmentList, setEquipmentList] = useState([]); const [dispatchHistories, setDispatchHistories] = useState([]); const [projects, setProjects] = useState([]); const [editLoading, setEditLoading] = useState(false); // Profile image upload const fileInputRef = useRef(null); // Confirmation dialogs within edit modal const [showRoleConfirm, setShowRoleConfirm] = useState(false); const [pendingRole, setPendingRole] = useState(null); const [showStatusConfirm, setShowStatusConfirm] = useState(false); const [pendingStatus, setPendingStatus] = useState(null); // Delete Confirmation State const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingEmployee, setDeletingEmployee] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const fetchEmployees = async () => { try { setLoading(true); const res = await employeeService.getAll({ size: 100 }); setEmployees(res.data.content); setError(null); } catch (err) { setError('Failed to load employees. Please try again.'); console.error(err); } finally { setLoading(false); } }; useEffect(() => { if (!user) return; if (user.roleCode === 'EMPLOYEE') { setError('Only managers and admins can view employee list.'); setLoading(false); return; } fetchEmployees(); }, [user]); const handleOpenModal = () => { setIsModalOpen(true); setFormData({ loginId: '', name: '', email: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE', dispatchStatus: 'WAITING', }); setModalError(null); }; const handleCloseModal = () => { setIsModalOpen(false); setFormData({ loginId: '', name: '', email: '', roleCode: 'EMPLOYEE', empStatus: 'ACTIVE', dispatchStatus: 'WAITING', }); setModalError(null); }; const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.loginId.trim() || !formData.name.trim()) { setModalError('Login ID and Name are required.'); return; } setIsSubmitting(true); setModalError(null); try { await employeeService.create(formData); alert('Employee added successfully!'); handleCloseModal(); await fetchEmployees(); } catch (err: any) { setModalError(err.response?.data?.message || 'Failed to add employee. Please try again.'); } finally { setIsSubmitting(false); } }; // Edit handlers const handleEdit = async (emp: Employee) => { setEditingEmployee(emp); setEditModalError(null); setEditLoading(true); // Populate basic info setEditBasic({ name: emp.name, email: emp.email || '', phone: emp.phone || '', joinedDate: emp.joinedDate || '', birthDate: emp.birthDate || '', education: emp.education || '', major: emp.major || '', graduationDate: emp.graduationDate || '', residence: emp.residence || '', profileImage: emp.profileImage || '', }); setEditStatus({ empStatus: emp.empStatus, leftDate: emp.leftDate || '', }); setEditRole(emp.roleCode); setEditDispatchStatus(emp.dispatchStatus); // Fetch related data in parallel try { const [equipRes, dispatchRes, projectsRes] = await Promise.allSettled([ apiClient.get(`/employees/${emp.empId}/equipment`), employeeService.getDispatchHistories(emp.empId), apiClient.get('/projects'), ]); if (equipRes.status === 'fulfilled' && equipRes.value.success) { setEquipmentList(equipRes.value.data || []); } else { setEquipmentList([]); } if (dispatchRes.status === 'fulfilled' && dispatchRes.value.success) { setDispatchHistories(dispatchRes.value.data || []); } else { setDispatchHistories([]); } if (projectsRes.status === 'fulfilled' && projectsRes.value.success) { setProjects(projectsRes.value.data || []); } else { setProjects([]); } } catch (err) { console.error('Failed to load edit modal data:', err); } finally { setEditLoading(false); } setIsEditModalOpen(true); }; const handleCloseEditModal = () => { setIsEditModalOpen(false); setEditingEmployee(null); setEditModalError(null); setEquipmentList([]); setDispatchHistories([]); setProjects([]); setShowRoleConfirm(false); setShowStatusConfirm(false); setPendingRole(null); setPendingStatus(null); }; const handleBasicInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setEditBasic((prev) => ({ ...prev, [name]: value })); }; const handleProfileImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !editingEmployee) return; try { const form = new FormData(); form.append('file', file); const uploadRes = await fetch('/api/v1/files/upload', { method: 'POST', body: form }); const uploadData = await uploadRes.json(); if (uploadData.success && uploadData.data) { await employeeService.updateProfileImage(editingEmployee.empId, uploadData.data); setEditBasic((prev) => ({ ...prev, profileImage: uploadData.data })); // Update local employee list setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, profileImage: uploadData.data } : emp ) ); } } catch (err: any) { setEditModalError(err.message || 'Failed to upload profile image.'); } // Reset input if (fileInputRef.current) fileInputRef.current.value = ''; }; const handleRemoveProfileImage = async () => { if (!editingEmployee) return; try { await employeeService.updateProfileImage(editingEmployee.empId, ''); setEditBasic((prev) => ({ ...prev, profileImage: '' })); setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, profileImage: null } : emp ) ); } catch (err: any) { setEditModalError(err.message || 'Failed to remove profile image.'); } }; const handleSaveBasicInfo = async () => { if (!editingEmployee) return; if (!editBasic.name.trim()) { setEditModalError('Name is required.'); return; } setIsUpdating(true); setEditModalError(null); try { const updateData: Record = { name: editBasic.name, email: editBasic.email, phone: editBasic.phone, joinedDate: editBasic.joinedDate, birthDate: editBasic.birthDate, education: editBasic.education, major: editBasic.major, graduationDate: editBasic.graduationDate, residence: editBasic.residence, profileImage: editBasic.profileImage, }; await employeeService.update(editingEmployee.empId, updateData as Partial); // Update local list setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, ...editBasic, profileImage: editBasic.profileImage } : emp ) ); alert('Basic information updated successfully!'); } catch (err: any) { setEditModalError(err.message || 'Failed to update basic information.'); } finally { setIsUpdating(false); } }; const handleSaveStatus = async () => { if (!editingEmployee) return; if (editStatus.empStatus === 'RESIGNED' && !editStatus.leftDate) { setEditModalError('Left date is required when changing to RESIGNED status.'); return; } setIsUpdating(true); setEditModalError(null); try { await apiClient.put(`/employees/${editingEmployee.empId}/status`, { empStatus: editStatus.empStatus, leftDate: editStatus.empStatus === 'RESIGNED' ? editStatus.leftDate : null, }); setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, empStatus: editStatus.empStatus, leftDate: editStatus.empStatus === 'RESIGNED' ? editStatus.leftDate : null } : emp ) ); setShowStatusConfirm(false); setPendingStatus(null); alert('Employee status updated successfully!'); } catch (err: any) { setEditModalError(err.message || 'Failed to update employee status.'); } finally { setIsUpdating(false); } }; const handleStatusChange = (newStatus: EmpStatus) => { if (newStatus === editingEmployee?.empStatus) return; if (newStatus === 'RESIGNED') { setPendingStatus(newStatus); setShowStatusConfirm(true); } else { setEditStatus((prev) => ({ ...prev, empStatus: newStatus })); // Auto-save for non-resigned changes setEditStatus((prev) => ({ ...prev, empStatus: newStatus })); // Trigger save setTimeout(() => { handleSaveStatus(); }, 0); } }; const confirmStatusChange = () => { if (pendingStatus) { setEditStatus((prev) => ({ ...prev, empStatus: pendingStatus })); setShowStatusConfirm(false); setTimeout(() => { handleSaveStatus(); }, 0); } }; const handleRoleChange = (newRole: RoleCode) => { if (newRole === editingEmployee?.roleCode) return; setPendingRole(newRole); setShowRoleConfirm(true); }; const confirmRoleChange = async () => { if (!editingEmployee || !pendingRole) return; setIsUpdating(true); setEditModalError(null); setShowRoleConfirm(false); try { await apiClient.put(`/employees/${editingEmployee.empId}/role`, { roleCode: pendingRole }); setEditRole(pendingRole); setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, roleCode: pendingRole! } : emp ) ); alert('Role updated successfully!'); } catch (err: any) { setEditModalError(err.message || 'Failed to update role.'); } finally { setIsUpdating(false); setPendingRole(null); } }; const handleDispatchStatusChange = async (newStatus: DispatchStatus) => { if (!editingEmployee || newStatus === editingEmployee.dispatchStatus) return; setIsUpdating(true); setEditModalError(null); try { await apiClient.put(`/employees/${editingEmployee.empId}/dispatch-status`, { dispatchStatus: newStatus }); setEditDispatchStatus(newStatus); setEmployees((prev) => prev.map((emp) => emp.empId === editingEmployee.empId ? { ...emp, dispatchStatus: newStatus } : emp ) ); } catch (err: any) { setEditModalError(err.message || 'Failed to update dispatch status.'); } finally { setIsUpdating(false); } }; // Delete handlers const handleDelete = (emp: Employee) => { setDeletingEmployee(emp); setIsDeleteDialogOpen(true); }; const handleCloseDeleteDialog = () => { setIsDeleteDialogOpen(false); setDeletingEmployee(null); }; const handleDeleteConfirm = async () => { if (!deletingEmployee) return; setIsDeleting(true); try { await employeeService.delete(deletingEmployee.empId); alert('Employee deleted successfully!'); handleCloseDeleteDialog(); await fetchEmployees(); } catch (err: any) { alert(err.response?.data?.message || 'Failed to delete employee. Please try again.'); handleCloseDeleteDialog(); } finally { setIsDeleting(false); } }; const [skillKeyword, setSkillKeyword] = useState(''); const [skillResults, setSkillResults] = useState([]); useEffect(() => { if (!skillKeyword.trim()) { setSkillResults([]); return; } const timer = setTimeout(() => { employeeService.search(skillKeyword.trim()).then((res) => { const empIds = new Set(res.data.map((r: any) => r.empId)); setSkillResults(employees.filter((e) => empIds.has(e.empId))); }).catch(() => {}); }, 300); return () => clearTimeout(timer); }, [skillKeyword, employees]); const displayList = skillKeyword.trim() ? skillResults : employees; const filtered = filter === 'all' ? displayList : displayList.filter((e) => e.empStatus === filter); const stats = [ { label: '전체', value: employees.length, color: 'text-text-main' }, { label: '재직', value: employees.filter((e) => e.empStatus === 'ACTIVE').length, color: 'text-status-resolved-text' }, { label: '파견중', value: employees.filter((e) => e.dispatchStatus === 'DISPATCHED').length, color: 'text-status-pending-text' }, { label: '휴직', value: employees.filter((e) => e.empStatus === 'ON_LEAVE').length, color: 'text-status-critical-text' }, ]; const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' }); }; const currentDispatch = dispatchHistories.find((d) => !d.endDate) || null; const pastDispatches = dispatchHistories.filter((d) => d.endDate); const getProjectById = (projectId: number) => { return projects.find((p) => p.projectId === projectId) || null; }; // Add Employee Modal if (isModalOpen) { return (

Add Employee

Create a new employee account

{modalError && (
{modalError}
)}
); } // Edit Employee Modal - REDESIGNED if (isEditModalOpen) { return (
{/* Header */}

Edit Employee

{editingEmployee?.name} ({editingEmployee?.loginId})

{editModalError && (
{editModalError}
)} {editLoading ? (
{[1, 2, 3, 4].map((i) => (
{[1, 2, 3, 4].map((j) => (
))}
))}
) : (
{/* SECTION 1: 기본 정보 */}

기본 정보

{/* Profile Image */}
{editBasic.profileImage ? ( {editBasic.name} ) : (
{editBasic.name[0]}
)}
{editBasic.profileImage && ( )}
{/* Basic Info Fields */}
{/* SECTION 2: 상태 & 권한 */}

상태 & 권한

{/* Employee Status */}
{/* Role */}
{/* Dispatch Status */}
{/* Left Date (shown when RESIGNED) */} {editStatus.empStatus === 'RESIGNED' && (
setEditStatus((prev) => ({ ...prev, leftDate: e.target.value }))} className="w-full px-3 py-2.5 rounded-xl bg-white border border-status-critical/30 text-text-main text-sm focus:outline-none focus:ring-2 focus:ring-status-critical transition-all" />
)}
{/* SECTION 3: 현재 지급 장비 */}

현재 지급 장비

{equipmentList.length === 0 ? (
No equipment currently assigned.
) : (
{equipmentList .filter((eq) => !eq.returnedDate) .map((eq) => ( ))}
자산코드 모델명 제조사 시리얼번호 지급일 상태
{eq.equipment?.assetCode || '-'} {eq.equipment?.modelName || '-'} {eq.equipment?.manufacturer || '-'} {eq.equipment?.serialNumber || '-'} {formatDate(eq.assignedDate)} 사용중
)}
{/* SECTION 4: 파견 정보 */}

파견 정보

{/* Current Dispatch */} {currentDispatch ? (
현재 파견중
프로젝트

{currentDispatch.projectName || '-'}

역할

{currentDispatch.roleTitle || '-'}

기간

{formatDate(currentDispatch.startDate)} ~ 진행중

근무지

{getProjectById(currentDispatch.projectId)?.workAddress || '-'}

) : (
현재 파견된 프로젝트가 없습니다.
)} {/* Dispatch History */} {pastDispatches.length > 0 && (
파견 이력 ({pastDispatches.length})
{pastDispatches.map((dh) => (

{dh.projectName || 'Project'}

{dh.roleTitle}

{formatDate(dh.startDate)} ~ {formatDate(dh.endDate)}

))}
)}
)} {/* Modal Footer */}
{/* Role Change Confirmation Dialog */} {showRoleConfirm && (

Confirm Role Change

Change role for {editingEmployee?.name}?

Role will change from{' '} {roleLabel[editingEmployee?.roleCode || 'EMPLOYEE'] || editingEmployee?.roleCode} {' '}to{' '} {roleLabel[pendingRole || 'EMPLOYEE'] || pendingRole}

)} {/* Status Change Confirmation Dialog (for RESIGNED) */} {showStatusConfirm && (

Confirm Status Change

Change status to RESIGNED?

This will mark {editingEmployee?.name} as resigned. Please ensure the left date is set.

)}
); } // Delete Confirmation Dialog if (isDeleteDialogOpen) { return (

Delete Employee

This action cannot be undone

Are you sure you want to delete {deletingEmployee?.name}?

This will soft-delete the employee record and remove their access to the system.

); } if (loading) { return (

Employees

Manage employee records, dispatch status, and role assignments.

{[1, 2, 3, 4].map((i) => (
))}
{[1, 2, 3, 4].map((i) => (
))}
{[1, 2, 3, 4, 5].map((i) => (
))}
); } if (error) { return (

Employees

Manage employee records, dispatch status, and role assignments.

{error}

); } if (employees.length === 0) { return (

Employees

Manage employee records, dispatch status, and role assignments.

No employees found

); } return (

Employees

Manage employee records, dispatch status, and role assignments.

{stats.map((s) => (

{s.label}

{s.value}

))}
{statusCodesLoading ? ( Loading... ) : ( empStatusCodes.map((code) => ( )) )}
setSkillKeyword(e.target.value)} className="w-full pl-9 pr-3 py-1.5 rounded-lg bg-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Search by name or skill..." />
{/* Desktop table */}
{filtered.map((emp, idx) => ( ))}
이름 권한 상태 이메일 액션
{emp.name[0]}

{emp.name}

{emp.email}

{roleLabel[emp.roleCode] || emp.roleCode} {emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'} {emp.dispatchStatus === 'DISPATCHED' && ( 파견중 )} {emp.email} {user?.roleCode === 'ADMIN' && (
)} {user?.roleCode !== 'ADMIN' && ( )}
{/* Mobile cards */}
{filtered.map((emp, idx) => (
{emp.name.charAt(0)}

{emp.name}

{roleLabel[emp.roleCode] || emp.roleCode}

{emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'}
Email: {emp.email}
상태: {emp.dispatchStatus === 'DISPATCHED' ? '파견중' : '대기'}
{user?.roleCode === 'ADMIN' && (
)}
))}
); }