From eb5734aad68508a71ed2c965ddab780bd94b163d Mon Sep 17 00:00:00 2001 From: pray4skylark Date: Fri, 5 Jun 2026 11:00:09 +0900 Subject: [PATCH] feat: enhanced employee edit modal (profile, equipment, dispatch, status) --- .../controller/EmployeeController.java | 48 +- .../EquipmentAssignmentRepository.java | 2 + .../com/srsystem/service/EmployeeService.java | 38 +- src/app/(main)/employees/page.tsx | 932 +++++++++++++++--- src/types/index.ts | 9 + 5 files changed, 872 insertions(+), 157 deletions(-) diff --git a/backend/src/main/java/com/srsystem/controller/EmployeeController.java b/backend/src/main/java/com/srsystem/controller/EmployeeController.java index 9237b4e..696de78 100644 --- a/backend/src/main/java/com/srsystem/controller/EmployeeController.java +++ b/backend/src/main/java/com/srsystem/controller/EmployeeController.java @@ -10,7 +10,11 @@ import com.srsystem.dto.UpdateAchievementsRequest; import com.srsystem.entity.DispatchHistory; import com.srsystem.entity.Employee; import com.srsystem.entity.EmployeeEmergencyContact; +import com.srsystem.entity.EquipmentAssignment; +import com.srsystem.entity.enums.DispatchStatus; import com.srsystem.entity.enums.EmpStatus; +import com.srsystem.entity.enums.RoleCode; +import com.srsystem.repository.EquipmentAssignmentRepository; import com.srsystem.service.DispatchHistoryService; import com.srsystem.service.EmployeeService; import com.srsystem.service.PortfolioService; @@ -42,6 +46,7 @@ public class EmployeeController { private final EmployeeService employeeService; private final DispatchHistoryService dispatchHistoryService; private final PortfolioService portfolioService; + private final EquipmentAssignmentRepository equipmentAssignmentRepository; @Operation(summary = "직원 목록 조회", description = "페이징된 직원 목록을 조회합니다. 상태 필터 옵션 포함") @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") @@ -86,7 +91,7 @@ public class EmployeeController { return ResponseEntity.ok(ApiResponse.ok("직원이 등록되었습니다", created)); } - @Operation(summary = "직원 정보 수정", description = "직원 정보를 수정합니다 (퇴사자는 수정 불가)") + @Operation(summary = "직원 정보 수정", description = "기본 정보(이름/이메일/전화번호/생년월일/학력/전공/졸업일/거주지/프로필사진) 수정") @PreAuthorize("hasRole('ADMIN')") @PutMapping("/{empId}") public ResponseEntity> update( @@ -96,6 +101,37 @@ public class EmployeeController { return ResponseEntity.ok(ApiResponse.ok("직원 정보가 수정되었습니다", updated)); } + @Operation(summary = "직원 재직 상태 변경", description = "ACTIVE/ON_LEAVE/RESIGNED + 퇴사일") + @PreAuthorize("hasRole('ADMIN')") + @PutMapping("/{empId}/status") + public ResponseEntity> updateStatus( + @PathVariable Long empId, @RequestBody Map body) { + EmpStatus status = EmpStatus.valueOf(body.get("empStatus")); + String leftDate = body.get("leftDate"); + Employee updated = employeeService.updateStatus(empId, status, leftDate); + return ResponseEntity.ok(ApiResponse.ok("상태가 변경되었습니다", updated)); + } + + @Operation(summary = "직원 권한 변경", description = "ADMIN/ MANAGER/ EMPLOYEE") + @PreAuthorize("hasRole('ADMIN')") + @PutMapping("/{empId}/role") + public ResponseEntity> updateRole( + @PathVariable Long empId, @RequestBody Map body) { + RoleCode role = RoleCode.valueOf(body.get("roleCode")); + Employee updated = employeeService.updateRole(empId, role); + return ResponseEntity.ok(ApiResponse.ok("권한이 변경되었습니다", updated)); + } + + @Operation(summary = "직원 파견 상태 변경", description = "WAITING/ DISPATCHED") + @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')") + @PutMapping("/{empId}/dispatch-status") + public ResponseEntity> updateDispatchStatus( + @PathVariable Long empId, @RequestBody Map body) { + DispatchStatus status = DispatchStatus.valueOf(body.get("dispatchStatus")); + Employee updated = employeeService.updateDispatchStatus(empId, status); + return ResponseEntity.ok(ApiResponse.ok("파견 상태가 변경되었습니다", updated)); + } + @Operation(summary = "직원 삭제", description = "직원을 삭제합니다 (Soft Delete, 퇴사자는 삭제 불가)") @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{empId}") @@ -154,8 +190,16 @@ public class EmployeeController { return ResponseEntity.ok(ApiResponse.ok("성과가 저장되었습니다", null)); } - // ---- Emergency Contacts ---- + @Operation(summary = "직원 지급 장비 조회", description = "직원에게 지급된 장비 목록을 조회합니다 (반납되지 않은)") + @PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')") + @Transactional(readOnly = true) + @GetMapping("/{empId}/equipment") + public ResponseEntity>> getEquipment(@PathVariable Long empId) { + List assignments = equipmentAssignmentRepository.findByUser_EmpIdAndReturnedDateIsNull(empId); + return ResponseEntity.ok(ApiResponse.ok(assignments)); + } + // ---- Emergency Contacts ---- @Operation(summary = "비상연락망 목록 조회", description = "직원의 비상연락망 목록을 조회합니다") @PreAuthorize("hasAnyRole('EMPLOYEE', 'MANAGER', 'ADMIN')") @GetMapping("/{empId}/emergency-contacts") diff --git a/backend/src/main/java/com/srsystem/repository/EquipmentAssignmentRepository.java b/backend/src/main/java/com/srsystem/repository/EquipmentAssignmentRepository.java index 292c4b6..6f82448 100644 --- a/backend/src/main/java/com/srsystem/repository/EquipmentAssignmentRepository.java +++ b/backend/src/main/java/com/srsystem/repository/EquipmentAssignmentRepository.java @@ -11,4 +11,6 @@ import java.util.Optional; public interface EquipmentAssignmentRepository extends JpaRepository { List findByEquipment_EquipmentId(Long equipmentId); Optional findByEquipment_EquipmentIdAndReturnedDateIsNull(Long equipmentId); + List findByUser_EmpIdAndReturnedDateIsNull(Long empId); + List findByUser_EmpIdOrderByAssignedDateDesc(Long empId); } diff --git a/backend/src/main/java/com/srsystem/service/EmployeeService.java b/backend/src/main/java/com/srsystem/service/EmployeeService.java index 7b87637..caa9c0a 100644 --- a/backend/src/main/java/com/srsystem/service/EmployeeService.java +++ b/backend/src/main/java/com/srsystem/service/EmployeeService.java @@ -5,7 +5,9 @@ import com.srsystem.dto.EmployeeSearchResult; import com.srsystem.dto.EmployeeSummary; import com.srsystem.entity.Employee; import com.srsystem.entity.EmployeeEmergencyContact; +import com.srsystem.entity.enums.DispatchStatus; import com.srsystem.entity.enums.EmpStatus; +import com.srsystem.entity.enums.RoleCode; import com.srsystem.exception.BusinessException; import com.srsystem.exception.ErrorCode; import com.srsystem.repository.DispatchHistoryRepository; @@ -18,6 +20,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -99,11 +102,7 @@ public class EmployeeService { employee.setName(updateRequest.getName()); employee.setEmail(updateRequest.getEmail()); employee.setPhone(updateRequest.getPhone()); - employee.setRoleCode(updateRequest.getRoleCode()); - employee.setEmpStatus(updateRequest.getEmpStatus()); - employee.setDispatchStatus(updateRequest.getDispatchStatus()); employee.setJoinedDate(updateRequest.getJoinedDate()); - employee.setLeftDate(updateRequest.getLeftDate()); employee.setBirthDate(updateRequest.getBirthDate()); employee.setEducation(updateRequest.getEducation()); employee.setMajor(updateRequest.getMajor()); @@ -111,13 +110,36 @@ public class EmployeeService { employee.setResidence(updateRequest.getResidence()); employee.setProfileImage(updateRequest.getProfileImage()); - if (updateRequest.getPassword() != null && !updateRequest.getPassword().isBlank()) { - employee.setPassword(passwordEncoder.encode(updateRequest.getPassword())); - } - return employee; } + @Transactional + public Employee updateStatus(Long empId, EmpStatus status, String leftDate) { + Employee employee = findById(empId); + employee.setEmpStatus(status); + if (leftDate != null && !leftDate.isBlank()) { + employee.setLeftDate(LocalDate.parse(leftDate)); + } + if (status == EmpStatus.RESIGNED && leftDate != null) { + employee.setLeftDate(LocalDate.parse(leftDate)); + } + return employeeRepository.save(employee); + } + + @Transactional + public Employee updateRole(Long empId, RoleCode role) { + Employee employee = findById(empId); + employee.setRoleCode(role); + return employeeRepository.save(employee); + } + + @Transactional + public Employee updateDispatchStatus(Long empId, DispatchStatus status) { + Employee employee = findById(empId); + employee.setDispatchStatus(status); + return employeeRepository.save(employee); + } + @Transactional public void delete(Long empId) { Employee employee = findById(empId); diff --git a/src/app/(main)/employees/page.tsx b/src/app/(main)/employees/page.tsx index 7a75a9c..466c112 100644 --- a/src/app/(main)/employees/page.tsx +++ b/src/app/(main)/employees/page.tsx @@ -1,12 +1,13 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, Plus, MoreHorizontal, X, Pencil, Trash2, AlertTriangle } from 'lucide-react'; +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 } from '@/types'; +import type { Employee, RoleCode, EmpStatus, DispatchStatus, DispatchHistory, Project, EquipmentAssignment } from '@/types'; const statusColor: Record = { ACTIVE: 'bg-status-resolved-bg text-status-resolved-text', @@ -50,14 +51,42 @@ export default function EmployeesPage() { const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [editModalError, setEditModalError] = useState(null); - const [editData, setEditData] = useState({ + const [editingEmployee, setEditingEmployee] = useState(null); + + // Edit Modal - Section data + const [editBasic, setEditBasic] = useState({ name: '', email: '', - roleCode: 'EMPLOYEE' as RoleCode, - empStatus: 'ACTIVE' as EmpStatus, - dispatchStatus: 'WAITING' as DispatchStatus, + phone: '', + joinedDate: '', + birthDate: '', + education: '', + major: '', + graduationDate: '', + residence: '', + profileImage: '', }); - const [editingEmployee, setEditingEmployee] = useState(null); + 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); @@ -145,57 +174,267 @@ export default function EmployeesPage() { }; // Edit handlers - const handleEdit = (emp: Employee) => { + const handleEdit = async (emp: Employee) => { setEditingEmployee(emp); - setEditData({ + setEditModalError(null); + setEditLoading(true); + + // Populate basic info + setEditBasic({ name: emp.name, email: emp.email || '', - roleCode: emp.roleCode, - empStatus: emp.empStatus, - dispatchStatus: emp.dispatchStatus, + phone: emp.phone || '', + joinedDate: emp.joinedDate || '', + birthDate: emp.birthDate || '', + education: emp.education || '', + major: emp.major || '', + graduationDate: emp.graduationDate || '', + residence: emp.residence || '', + profileImage: emp.profileImage || '', }); - setEditModalError(null); + 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); - setEditData({ - name: '', - email: '', - roleCode: 'EMPLOYEE', - empStatus: 'ACTIVE', - dispatchStatus: 'WAITING', - }); setEditModalError(null); + setEquipmentList([]); + setDispatchHistories([]); + setProjects([]); + setShowRoleConfirm(false); + setShowStatusConfirm(false); + setPendingRole(null); + setPendingStatus(null); }; - const handleEditInputChange = (e: React.ChangeEvent) => { + const handleBasicInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setEditData((prev) => ({ ...prev, [name]: value })); + setEditBasic((prev) => ({ ...prev, [name]: value })); }; - const handleUpdateSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!editData.name.trim()) { + 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; } - if (!editingEmployee) return; - setIsUpdating(true); setEditModalError(null); try { - await employeeService.update(editingEmployee.empId, editData); - alert('Employee updated successfully!'); - handleCloseEditModal(); - await fetchEmployees(); + 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.response?.data?.message || 'Failed to update employee. Please try again.'); + 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); } @@ -254,6 +493,18 @@ export default function EmployeesPage() { { 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 ( @@ -396,129 +647,516 @@ export default function EmployeesPage() { ); } - // Edit Employee Modal + // Edit Employee Modal - REDESIGNED if (isEditModalOpen) { return ( -
-
- - -

Edit Employee

-

Update employee information

- -
+
+
+ {/* Header */} +
- - +

Edit Employee

+

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

- -
- - + +
+ + {editModalError && ( +
+ {editModalError}
- -
- - + + {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)} +

+
+
+ ))} +
+
+ )} + +
+ + +
- - {editModalError && ( -
- {editModalError} -
- )} - -
- - -
- + )} + + {/* 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. +

+
+
+ + +
+
+
+ )}
); } diff --git a/src/types/index.ts b/src/types/index.ts index 2d51f88..01b4931 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -225,6 +225,15 @@ export interface EquipmentAssignment { returnedDate: string | null; condOnAssign: string; condOnReturn: string | null; + equipment?: { + equipmentId: number; + assetCode: string; + manufacturer: string; + modelName: string; + serialNumber: string; + equipmentType: string; + status: string; + }; } export interface EquipmentMaintenance {