diff --git a/src/app/(main)/employees/page.tsx b/src/app/(main)/employees/page.tsx index 466c112..4171b07 100644 --- a/src/app/(main)/employees/page.tsx +++ b/src/app/(main)/employees/page.tsx @@ -1,13 +1,13 @@ 'use client'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } 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 { Search, X, Camera, Phone, Mail, Calendar, MapPin, GraduationCap, 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, DispatchHistory, Project, EquipmentAssignment } from '@/types'; +import type { Employee, RoleCode, EmpStatus, DispatchStatus } from '@/types'; const statusColor: Record = { ACTIVE: 'bg-status-resolved-bg text-status-resolved-text', @@ -34,64 +34,11 @@ export default function EmployeesPage() { 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); + // Detail Modal State + const [selectedEmployee, setSelectedEmployee] = useState(null); + const [detailDispatch, setDetailDispatch] = useState([]); + const [detailEquipment, setDetailEquipment] = useState([]); + const [detailLoading, setDetailLoading] = useState(false); const fetchEmployees = async () => { try { @@ -119,354 +66,32 @@ export default function EmployeesPage() { 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); + const openDetail = async (emp: Employee) => { + setSelectedEmployee(emp); + setDetailLoading(true); + setDetailDispatch([]); + setDetailEquipment([]); 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'), + const [dispatchRes, equipRes] = await Promise.all([ + employeeService.getDispatchHistories(emp.empId).catch(() => null), + apiClient.get(`/employees/${emp.empId}/equipment`).catch(() => null), ]); - 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); + if (dispatchRes?.data) setDetailDispatch(dispatchRes.data); + if (equipRes?.data) setDetailEquipment(equipRes.data); + } catch { + // silently ignore } - setIsEditModalOpen(true); + setDetailLoading(false); }; - 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 closeDetail = () => { + setSelectedEmployee(null); + setDetailDispatch([]); + setDetailEquipment([]); + setDetailLoading(false); }; const [skillKeyword, setSkillKeyword] = useState(''); @@ -498,711 +123,235 @@ export default function EmployeesPage() { 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; + const educationLabel: Record = { + HIGH_SCHOOL: '고졸', + ASSOCIATE: '전문대', + BACHELOR: '학사', + MASTER: '석사', + DOCTORATE: '박사', }; - // Add Employee Modal - if (isModalOpen) { - return ( -
-
- - -

Add Employee

-

Create a new employee account

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
- - -
- -
- - -
-
- - {modalError && ( -
- {modalError} -
- )} - -
- - -
-
-
-
- ); - } + // ── 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); - // Edit Employee Modal - REDESIGNED - if (isEditModalOpen) { return ( -
-
+
+
e.stopPropagation()} + > {/* Header */}
-
-

Edit Employee

-

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

-
+

Employee Detail

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

- - 기본 정보 -

- - {/* Profile Image */} -
-
- {editBasic.profileImage ? ( - {editBasic.name} - ) : ( -
- {editBasic.name[0]} -
- )} -
-
- + {emp.profileImage ? ( + {emp.name} - - {editBasic.profileImage && ( - - )} + ) : ( +
+ {emp.name[0]} +
+ )} +
+

{emp.name}

+
+ + {roleLabel[emp.roleCode] || emp.roleCode} + + + {emp.empStatus === 'ACTIVE' ? '재직' : emp.empStatus === 'ON_LEAVE' ? '휴직' : '퇴사'} + + {emp.dispatchStatus === 'DISPATCHED' && ( + + 파견중 + + )} +
- {/* Basic Info Fields */} -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - + {/* Contact & Personal Info */} +
+ {emp.email && ( +
+ + {emp.email} +
+ )} + {emp.phone && ( +
+ + {emp.phone} +
+ )} + {emp.birthDate && ( +
+ + {formatDate(emp.birthDate)} +
+ )} + {emp.residence && ( +
+ + {emp.residence} +
+ )} + {emp.education && ( +
+ + {educationLabel[emp.education] || emp.education}{emp.major ? ` · ${emp.major}` : ''} +
+ )} + {emp.graduationDate && ( +
+ + 졸업 {formatDate(emp.graduationDate)} +
+ )} +
+
+ + {/* Current Dispatch */} + {currentDispatch && ( +
+
+ + 현재 파견 +
+
+
+

{currentDispatch.projectName || 'Project'}

+ + {formatDate(currentDispatch.startDate)} ~ 진행중 + +
+ {currentDispatch.roleTitle && ( + + {currentDispatch.roleTitle} + + )}
+ )} -
+ {/* Quick Links */} +
+
빠른 이동
+
+ + + +
- {/* 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. + {/* Assigned Equipment */} +
+
+ + 지급 장비 +
+ {activeEquipments.length === 0 ? ( +
+ 현재 지급된 장비가 없습니다.
) : ( -
+
- - - - - - - + + + + + - {equipmentList - .filter((eq) => !eq.returnedDate) - .map((eq) => ( - - - - - - - - - ))} + {activeEquipments.map((eq: any) => ( + + + + + + + ))}
자산코드모델명제조사시리얼번호지급일상태
자산코드모델명제조사지급일
{eq.equipment?.assetCode || '-'}{eq.equipment?.modelName || '-'}{eq.equipment?.manufacturer || '-'}{eq.equipment?.serialNumber || '-'}{formatDate(eq.assignedDate)} - - 사용중 - -
{eq.equipment?.assetCode || '-'}{eq.equipment?.modelName || '-'}{eq.equipment?.manufacturer || '-'}{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. -

-
- -
- - -
); @@ -1279,10 +428,6 @@ export default function EmployeesPage() {

Employees

Manage employee records, dispatch status, and role assignments.

-

No employees found

@@ -1300,10 +445,6 @@ export default function EmployeesPage() {

Employees

Manage employee records, dispatch status, and role assignments.

-
@@ -1369,7 +510,11 @@ export default function EmployeesPage() { {filtered.map((emp, idx) => ( - + openDetail(emp)} + className="hover:bg-gray-50 transition-colors cursor-pointer" + >
@@ -1394,33 +539,7 @@ export default function EmployeesPage() { {emp.email} - {user?.roleCode === 'ADMIN' && ( -
- - -
- )} - {user?.roleCode !== 'ADMIN' && ( - - )} + View details → ))} @@ -1431,7 +550,11 @@ export default function EmployeesPage() { {/* Mobile cards */}
{filtered.map((emp, idx) => ( -
+
openDetail(emp)} + className="bg-surface rounded-2xl border border-border p-4 space-y-2 cursor-pointer hover:bg-gray-50 transition-colors" + >
@@ -1450,24 +573,6 @@ export default function EmployeesPage() {
Email: {emp.email}
상태: {emp.dispatchStatus === 'DISPATCHED' ? '파견중' : '대기'}
- {user?.roleCode === 'ADMIN' && ( -
- - -
- )}
))}