diff --git a/src/components/hr/EmployeeManagement/index.tsx b/src/components/hr/EmployeeManagement/index.tsx index 35388a6c..fdce2064 100644 --- a/src/components/hr/EmployeeManagement/index.tsx +++ b/src/components/hr/EmployeeManagement/index.tsx @@ -77,9 +77,9 @@ export function EmployeeManagement() { const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; - // 날짜 범위 상태 (Input type="date" 용) - const [startDate, setStartDate] = useState('2025-12-01'); - const [endDate, setEndDate] = useState('2025-12-31'); + // 날짜 범위 상태 (Input type="date" 용) - 초기값 비움: 전체 기간 조회 + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); // 필터 및 정렬 상태 const [filterOption, setFilterOption] = useState('all'); diff --git a/src/components/production/ProductionDashboard/types.ts b/src/components/production/ProductionDashboard/types.ts index 7b832501..04f429ad 100644 --- a/src/components/production/ProductionDashboard/types.ts +++ b/src/components/production/ProductionDashboard/types.ts @@ -30,6 +30,10 @@ export interface WorkOrder { delayDays?: number; // 지연 일수 instruction?: string; // 지시사항 salesOrderNo?: string; // 수주번호 + teamId?: number | null; // 배정 부서 ID (work_orders.team_id) + teamName?: string; // 배정 부서명 + processDepartment?: string; // 공정 담당부서명 (processes.department) + scheduledDate?: string; // 생산 예정일 (YYYY-MM-DD) createdAt: string; // 공정 설정 (작업자 화면용) processOptions?: { diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts index a710dfd7..3dc71e45 100644 --- a/src/components/production/WorkerScreen/actions.ts +++ b/src/components/production/WorkerScreen/actions.ts @@ -9,6 +9,7 @@ import { executeServerAction } from '@/lib/api/execute-server-action'; +import { buildApiUrl } from '@/lib/api/query-params'; import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types'; import type { WorkItemData, WorkStepData, ProcessTab } from './types'; @@ -40,7 +41,10 @@ interface WorkOrderApiItem { client?: { id: number; name: string }; root_nodes_count?: number; }; + team_id?: number | null; + team?: { id: number; name: string } | null; assignee?: { id: number; name: string }; + assignees?: { id: number; user_id: number; user?: { id: number; name: string } }[]; items?: { id: number; item_name: string; @@ -172,7 +176,9 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder { processName: processInfo.name, client: api.sales_order?.client?.name || '-', projectName: api.project_name || '-', - assignees: api.assignee ? [api.assignee.name] : [], + assignees: api.assignees?.length + ? api.assignees.map((a) => a.user?.name || '').filter(Boolean) + : api.assignee ? [api.assignee.name] : [], quantity: totalQuantity, shutterCount: nodeGroups.length || api.sales_order?.root_nodes_count || 0, dueDate, @@ -183,6 +189,10 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder { delayDays, instruction: api.memo || undefined, salesOrderNo: api.sales_order?.order_no || undefined, + teamId: api.team_id ?? null, + teamName: api.team?.name || undefined, + processDepartment: api.process?.department || undefined, + scheduledDate: api.scheduled_date || undefined, createdAt: api.created_at, processOptions: { needsInspection: api.process?.options?.needs_inspection ?? false, @@ -782,4 +792,52 @@ export async function saveInspectionDocument( errorMessage: '검사 문서 동기화에 실패했습니다.', }); return { success: result.success, data: result.data, error: result.error }; +} + +// ===== 부서 목록 조회 (작업 정보용) ===== +export interface DepartmentOption { + id: number; + name: string; +} + +export async function getDepartments(): Promise<{ + success: boolean; + data: DepartmentOption[]; + error?: string; +}> { + interface DeptApiItem { id: number; name: string; parent_id?: number | null; [key: string]: unknown } + const result = await executeServerAction<{ data: DeptApiItem[] } | DeptApiItem[]>({ + url: buildApiUrl('/api/v1/departments', { per_page: 100 }), + errorMessage: '부서 목록 조회에 실패했습니다.', + }); + if (!result.success || !result.data) return { success: false, data: [], error: result.error }; + const list = Array.isArray(result.data) ? result.data : (result.data.data || []); + return { + success: true, + data: list.map((d) => ({ id: d.id, name: d.name })), + }; +} + +// ===== 부서별 사용자 목록 조회 ===== +export interface DepartmentUser { + id: number; + name: string; +} + +export async function getDepartmentUsers(departmentId: number): Promise<{ + success: boolean; + data: DepartmentUser[]; + error?: string; +}> { + interface UserApiItem { id: number; name: string; [key: string]: unknown } + const result = await executeServerAction<{ data: UserApiItem[] } | UserApiItem[]>({ + url: buildApiUrl(`/api/v1/departments/${departmentId}/users`), + errorMessage: '부서 사용자 목록 조회에 실패했습니다.', + }); + if (!result.success || !result.data) return { success: false, data: [], error: result.error }; + const list = Array.isArray(result.data) ? result.data : (result.data.data || []); + return { + success: true, + data: list.map((u) => ({ id: u.id, name: u.name })), + }; } \ No newline at end of file diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 6c4ca6b5..60a54fba 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -40,8 +40,8 @@ import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; -import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate, getStepProgress, toggleStepProgress, deleteMaterialInput, updateMaterialInput } from './actions'; -import type { StepProgressItem } from './actions'; +import { getMyWorkOrders, completeWorkOrder, saveItemInspection, getWorkOrderInspectionData, saveInspectionDocument, getInspectionTemplate, getStepProgress, toggleStepProgress, deleteMaterialInput, updateMaterialInput, getDepartments, getDepartmentUsers } from './actions'; +import type { StepProgressItem, DepartmentOption, DepartmentUser } from './actions'; import type { InspectionTemplateData } from './types'; import { getProcessList } from '@/components/process-management/actions'; import type { InspectionSetting, Process } from '@/types/process'; @@ -320,6 +320,8 @@ export default function WorkerScreen() { const [departmentId, setDepartmentId] = useState(''); const [productionManagerId, setProductionManagerId] = useState(''); const [productionDate, setProductionDate] = useState(''); + const [departmentList, setDepartmentList] = useState([]); + const [departmentUsers, setDepartmentUsers] = useState([]); // 좌측 사이드바 const [selectedSidebarOrderId, setSelectedSidebarOrderId] = useState(''); @@ -352,8 +354,29 @@ export default function WorkerScreen() { useEffect(() => { loadData(); + // 부서 목록 로드 + getDepartments().then((res) => { + if (res.success) setDepartmentList(res.data); + }); }, [loadData]); + // 부서 선택 시 해당 부서 사용자 목록 로드 + useEffect(() => { + if (!departmentId) { + setDepartmentUsers([]); + setProductionManagerId(''); + return; + } + getDepartmentUsers(Number(departmentId)).then((res) => { + if (res.success) { + setDepartmentUsers(res.data); + } else { + setDepartmentUsers([]); + } + setProductionManagerId(''); + }); + }, [departmentId]); + // PC에서 사이드바 sticky 동작을 위해 main의 overflow 임시 해제 useEffect(() => { const mainEl = document.querySelector('main'); @@ -531,12 +554,24 @@ export default function WorkerScreen() { return; } - // 우선순위 순서: urgent → priority → normal + // API 작업지시 우선 선택 (부서/담당자 연동을 위해) + if (apiSidebarOrders.length > 0) { + const firstApi = apiSidebarOrders[0]; + setSelectedSidebarOrderId(firstApi.id); + if (activeProcessTabKey === 'slat') { + setSlatSubMode(firstApi.subType === 'jointbar' ? 'jointbar' : 'normal'); + } + if (activeProcessTabKey === 'bending') { + setBendingSubMode(firstApi.subType === 'wip' ? 'wip' : 'normal'); + } + return; + } + + // API 작업지시 없으면 목업에서 우선순위 순서: urgent → priority → normal for (const group of PRIORITY_GROUPS) { const first = allOrders.find((o) => o.priority === group.key); if (first) { setSelectedSidebarOrderId(first.id); - // subType에 따라 서브모드도 설정 if (activeProcessTabKey === 'slat') { setSlatSubMode(first.subType === 'jointbar' ? 'jointbar' : 'normal'); } @@ -796,6 +831,29 @@ export default function WorkerScreen() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSidebarOrderId, workItems.length]); + // ===== 작업지시 변경 시 작업 정보 자동 세팅 ===== + useEffect(() => { + const apiOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId); + if (apiOrder) { + // 부서 세팅: 1순위 work_orders.team_id → 2순위 process.department(부서명 매칭) + if (apiOrder.teamId) { + setDepartmentId(String(apiOrder.teamId)); + } else if (apiOrder.processDepartment && departmentList.length > 0) { + const matched = departmentList.find((d) => d.name === apiOrder.processDepartment); + setDepartmentId(matched ? String(matched.id) : ''); + } else { + setDepartmentId(''); + } + // 생산일자 세팅 + setProductionDate(apiOrder.scheduledDate || ''); + } else { + setDepartmentId(''); + setProductionManagerId(''); + setProductionDate(''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedSidebarOrderId, filteredWorkOrders, departmentList]); + // ===== 수주 정보 (사이드바 선택 항목 기반) ===== const orderInfo = useMemo(() => { // 1. 선택된 API 작업지시에서 찾기 @@ -1427,7 +1485,7 @@ export default function WorkerScreen() { - {/* 작업 정보 - 부서 필드 추가 */} + {/* 작업 정보 - API 연동 */}

작업 정보

@@ -1435,6 +1493,7 @@ export default function WorkerScreen() {