'use client'; import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Search, X, Camera, Phone, Mail, Briefcase, Ticket, FileText, ExternalLink } 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 } from '@/types'; import EmployeeEditModal from '@/components/employees/EmployeeEditModal'; 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])); // Detail Modal State const [selectedEmployee, setSelectedEmployee] = useState(null); const [editingEmployee, setEditingEmployee] = useState(null); const [detailDispatch, setDetailDispatch] = useState([]); const [detailEquipment, setDetailEquipment] = useState([]); const [detailEmergencyContacts, setDetailEmergencyContacts] = useState([]); const [detailLoading, setDetailLoading] = useState(false); // Emergency Contact Form State const [showContactModal, setShowContactModal] = useState(false); const [contactEditing, setContactEditing] = useState(null); // null if creating const [contactRel, setContactRel] = useState(''); const [contactName, setContactName] = useState(''); const [contactPhone, setContactPhone] = useState(''); const [contactSubmitting, setContactSubmitting] = useState(false); // Phone Reveal State const [showPhoneDialog, setShowPhoneDialog] = useState(false); const [phoneReason, setPhoneReason] = useState(''); const [revealedPhone, setRevealedPhone] = useState(''); 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 loadEmergencyContacts = async (empId: number) => { try { const res = await apiClient.get(`/employees/${empId}/emergency-contacts`); if (res?.data) setDetailEmergencyContacts(res.data); } catch { setDetailEmergencyContacts([]); } }; const openDetail = async (emp: Employee) => { setSelectedEmployee(emp); setDetailLoading(true); setDetailDispatch([]); setDetailEquipment([]); setDetailEmergencyContacts([]); setRevealedPhone(''); setPhoneReason(''); try { const [dispatchRes, equipRes, contactRes] = await Promise.all([ employeeService.getDispatchHistories(emp.empId).catch(() => null), apiClient.get(`/employees/${emp.empId}/equipment`).catch(() => null), apiClient.get(`/employees/${emp.empId}/emergency-contacts`).catch(() => null), ]); if (dispatchRes?.data) setDetailDispatch(dispatchRes.data); if (equipRes?.data) setDetailEquipment(equipRes.data); if (contactRes?.data) setDetailEmergencyContacts(contactRes.data); } catch { // silently ignore } setDetailLoading(false); }; const closeDetail = () => { setSelectedEmployee(null); setDetailDispatch([]); setDetailEquipment([]); setDetailEmergencyContacts([]); setDetailLoading(false); setShowPhoneDialog(false); setPhoneReason(''); setRevealedPhone(''); setShowContactModal(false); setContactEditing(null); }; const openContactForm = (contact?: any) => { if (contact) { setContactEditing(contact); setContactRel(contact.relationship || ''); setContactName(contact.contactName || ''); setContactPhone(contact.phone || ''); } else { setContactEditing(null); setContactRel(''); setContactName(''); setContactPhone(''); } setShowContactModal(true); }; const handleSaveContact = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedEmployee || !contactRel.trim() || !contactName.trim() || !contactPhone.trim()) return; setContactSubmitting(true); try { const payload = { relationship: contactRel.trim(), contactName: contactName.trim(), phone: contactPhone.trim(), }; if (contactEditing?.contactId) { await apiClient.put(`/employees/${selectedEmployee.empId}/emergency-contacts/${contactEditing.contactId}`, payload); } else { await apiClient.post(`/employees/${selectedEmployee.empId}/emergency-contacts`, payload); } await loadEmergencyContacts(selectedEmployee.empId); setShowContactModal(false); } catch (err) { console.error('Failed to save emergency contact', err); } finally { setContactSubmitting(false); } }; const handleDeleteContact = async (contactId: number) => { if (!selectedEmployee || !confirm('이 비상연락처를 삭제하시겠습니까?')) return; try { await apiClient.delete(`/employees/${selectedEmployee.empId}/emergency-contacts/${contactId}`); await loadEmergencyContacts(selectedEmployee.empId); } catch (err) { console.error('Failed to delete emergency contact', err); } }; 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' }); }; // ── Detail Modal ── if (selectedEmployee) { const emp = selectedEmployee; const currentDispatch = detailDispatch.find((d: any) => !d.endDate) || null; const activeEquipments = (detailEquipment || []).filter((eq: any) => !eq.returnedDate && eq.equipment); const canManageContacts = user?.roleCode === 'ADMIN' || user?.roleCode === 'MANAGER'; return (
e.stopPropagation()} > {/* Header */}

직원 상세 정보

{detailLoading ? (
{[1, 2, 3, 4].map((i) => (
))}
) : (
{/* Profile Card */}
{emp.profileImage ? ( {emp.name} ) : (
{emp.name[0]}
)}

{emp.name}

{roleLabel[emp.roleCode] || emp.roleCode} {emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'} {emp.dispatchStatus === 'DISPATCHED' && ( 파견중 )}
{/* Contact & Personal Info */}
{emp.email && (
{emp.email}
)}
{emp.phone ? ( revealedPhone ? ( {revealedPhone} ) : ( ) ) : ( - )}
{/* Emergency Contacts Section */}
비상 연락망
{canManageContacts && ( )}
{detailEmergencyContacts.length === 0 ? (
등록된 비상연락처가 없습니다.
) : (
{detailEmergencyContacts.map((contact: any) => (

{contact.contactName} ({contact.relationship})

{contact.phone}

{canManageContacts && (
·
)}
))}
)}
{/* Current Dispatch */} {currentDispatch && (
현재 파견

{currentDispatch.projectName || 'Project'}

{formatDate(currentDispatch.startDate)} ~ 진행중
{currentDispatch.roleTitle && ( {currentDispatch.roleTitle} )}
)} {/* Quick Links */}
빠른 이동
{/* Assigned Equipment */}
지급 장비
{activeEquipments.length === 0 ? (
현재 지급된 장비가 없습니다.
) : (
{activeEquipments.map((eq: any) => ( ))}
자산코드 모델명 제조사 지급일
{eq.equipment?.assetCode || '-'} {eq.equipment?.modelName || '-'} {eq.equipment?.manufacturer || '-'} {formatDate(eq.assignedDate)}
)}
)}
{/* Emergency Contact Modal */} {showContactModal && (
setShowContactModal(false)}>
e.stopPropagation()} >

{contactEditing ? '비상연락처 수정' : '비상연락처 추가'}

setContactRel(e.target.value)} className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" placeholder="관계 입력" />
setContactName(e.target.value)} className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" placeholder="성명 입력" />
setContactPhone(e.target.value)} className="w-full border border-border rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" placeholder="010-0000-0000" />
)} {/* Phone Reveal Dialog */} {showPhoneDialog && (
{ setShowPhoneDialog(false); setPhoneReason(''); }}>
e.stopPropagation()}>

전화번호 조회

조회 사유를 입력해주세요.