diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx index 37efcff9..db361818 100644 --- a/src/components/production/WorkOrders/WorkOrderDetail.tsx +++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2 } from 'lucide-react'; +import { FileText, List, AlertTriangle, Play, CheckCircle2, Loader2, Undo2 } from 'lucide-react'; import { ContentLoadingSpinner } from '@/components/ui/loading-spinner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -323,25 +323,6 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { ); } - // 작업일지용 WorkOrder 변환 (기존 WorkLogModal 타입에 맞춤) - const workLogOrder = { - id: order.id, - orderNo: order.workOrderNo, - productName: order.items[0]?.productName || '-', - client: order.client, - projectName: order.projectName, - dueDate: order.dueDate, - quantity: order.items.reduce((sum, item) => sum + item.quantity, 0), - progress: order.currentStep * 20, // 대략적인 진행률 - process: order.processType as 'screen' | 'slat' | 'bending', - assignees: order.assignees && order.assignees.length > 0 - ? order.assignees.map(a => a.name) - : [order.assignee], - instruction: order.note || '', - status: 'in_progress' as const, - priority: order.priority <= 3 ? 'high' : order.priority <= 6 ? 'medium' : 'low', - }; - return ( {/* 헤더 */} @@ -469,36 +450,70 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { {item.specification} {item.quantity} - {item.status === 'waiting' && ( - - )} - {item.status === 'in_progress' && ( - - )} +
+ {item.status === 'waiting' && ( + + )} + {item.status === 'in_progress' && ( + <> + + + + )} + {item.status === 'completed' && ( + + )} +
))} @@ -522,7 +537,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
); diff --git a/src/components/production/WorkOrders/types.ts b/src/components/production/WorkOrders/types.ts index a69c702f..9374a883 100644 --- a/src/components/production/WorkOrders/types.ts +++ b/src/components/production/WorkOrders/types.ts @@ -238,6 +238,7 @@ export interface WorkOrderItemApi { quantity: number; unit: string | null; sort_order: number; + status: 'waiting' | 'in_progress' | 'completed'; created_at: string; updated_at: string; } @@ -401,7 +402,7 @@ export function transformApiToFrontend(api: WorkOrderApi): WorkOrder { items: (api.items || []).map((item, idx) => ({ id: String(item.id), no: idx + 1, - status: 'waiting' as ItemStatus, + status: (item.status as ItemStatus) || 'waiting', productName: item.item_name, floorCode: '-', specification: item.specification || '-', diff --git a/src/components/production/WorkerScreen/WorkLogModal.tsx b/src/components/production/WorkerScreen/WorkLogModal.tsx index 2315f431..2556acea 100644 --- a/src/components/production/WorkerScreen/WorkLogModal.tsx +++ b/src/components/production/WorkerScreen/WorkLogModal.tsx @@ -5,9 +5,11 @@ * * - 헤더: sam-design 작업일지 스타일 * - 내부 문서: 스크린샷 기준 작업일지 양식 + * - API 연동 완료 (2025-01-14) */ -import { Printer, X } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { Printer, X, Loader2 } from 'lucide-react'; import { Dialog, DialogContent, @@ -16,21 +18,114 @@ import { import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; import { Button } from '@/components/ui/button'; import { printArea } from '@/lib/print-utils'; -import type { WorkOrder } from '../ProductionDashboard/types'; -import { PROCESS_LABELS } from '../ProductionDashboard/types'; +import { getWorkOrderById } from '../WorkOrders/actions'; +import type { WorkOrder, WorkOrderItem } from '../WorkOrders/types'; +import { ITEM_STATUS_LABELS } from '../WorkOrders/types'; interface WorkLogModalProps { open: boolean; onOpenChange: (open: boolean) => void; - order: WorkOrder | null; + workOrderId: string | null; } -export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) { +// 작업 통계 타입 +interface WorkStats { + orderQty: number; + completedQty: number; + inProgressQty: number; + waitingQty: number; + progress: number; +} + +// 품목 데이터에서 작업 통계 계산 +function calculateWorkStats(items: WorkOrderItem[]): WorkStats { + const orderQty = items.length; + const completedQty = items.filter(i => i.status === 'completed').length; + const inProgressQty = items.filter(i => i.status === 'in_progress').length; + const waitingQty = items.filter(i => i.status === 'waiting').length; + const progress = orderQty > 0 ? Math.round((completedQty / orderQty) * 100) : 0; + + return { + orderQty, + completedQty, + inProgressQty, + waitingQty, + progress, + }; +} + +export function WorkLogModal({ open, onOpenChange, workOrderId }: WorkLogModalProps) { + const [order, setOrder] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // 모달 열릴 때 데이터 fetch + useEffect(() => { + if (open && workOrderId) { + setIsLoading(true); + setError(null); + + getWorkOrderById(workOrderId) + .then((result) => { + if (result.success && result.data) { + setOrder(result.data); + } else { + setError(result.error || '데이터를 불러올 수 없습니다.'); + } + }) + .catch(() => { + setError('서버 오류가 발생했습니다.'); + }) + .finally(() => { + setIsLoading(false); + }); + } else if (!open) { + // 모달 닫힐 때 상태 초기화 + setOrder(null); + setError(null); + } + }, [open, workOrderId]); + const handlePrint = () => { printArea({ title: '작업일지 인쇄' }); }; - if (!order) return null; + if (!workOrderId) return null; + + // 로딩 상태 + if (isLoading) { + return ( + + + + 작업일지 로딩 중 + +
+ +
+
+
+ ); + } + + // 에러 상태 + if (error || !order) { + return ( + + + + 작업일지 오류 + +
+

{error || '데이터를 불러올 수 없습니다.'}

+ +
+
+
+ ); + } const today = new Date().toLocaleDateString('ko-KR', { year: 'numeric', @@ -38,42 +133,30 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) { day: '2-digit', }).replace(/\. /g, '-').replace('.', ''); - const documentNo = `WL-${order.process.toUpperCase().slice(0, 3)}`; - const lotNo = `KD-TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`; + const documentNo = `WL-${order.processCode.toUpperCase().slice(0, 3)}`; - // 샘플 품목 데이터 (스크린샷 기준) - const items = [ - { no: 1, name: '스크린 사타 (표준형)', location: '1층/A-01', spec: '3000×2500', qty: 1, status: '대기' }, - { no: 2, name: '스크린 사타 (표준형)', location: '2층/A-02', spec: '3000×2500', qty: 1, status: '대기' }, - { no: 3, name: '스크린 사타 (표준형)', location: '3층/A-03', spec: '-', qty: '-', status: '대기' }, - ]; + // 품목 데이터 + const items = order.items || []; - // 작업내역 데이터 (스크린샷 기준) - const workStats = { - workType: '필름 스크린', - workWidth: '1016mm', - general: 3, - ironing: 3, - sandblast: 3, - packing: 1, - orderQty: 3, - completedQty: 1, - progress: 33, - }; + // 작업 통계 계산 + const workStats = calculateWorkStats(items); + + // 주 담당자 + const primaryAssignee = order.assignees?.find(a => a.isPrimary)?.name || order.assignee || '-'; return ( {/* 접근성을 위한 숨겨진 타이틀 */} - 작업일지 - {order.orderNo} + 작업일지 - {order.workOrderNo} {/* 모달 헤더 - sam-design 스타일 (인쇄 시 숨김) */}
작업일지 - {PROCESS_LABELS[order.process]} 생산부서 + {order.processName} 생산부서 ({documentNo}) @@ -109,7 +192,7 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) {

작 업 일 지

{documentNo}

-

{PROCESS_LABELS[order.process]} 생산부서

+

{order.processName} 생산부서

{/* 우측: 결재라인 */} @@ -131,7 +214,7 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) { {/* 두 번째 행: 이름 + 날짜 */} -
{order.assignees[0] || '-'}
+
{primaryAssignee}
{new Date().toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }).replace('. ', '/').replace('.', '')}
@@ -180,7 +263,7 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) {
LOT NO.
-
{lotNo}
+
{order.lotNo}
@@ -191,18 +274,18 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) { 납기일
- {new Date(order.dueDate).toLocaleDateString('ko-KR', { + {order.dueDate !== '-' ? new Date(order.dueDate).toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit', - }).replace(/\. /g, '-').replace('.', '')} + }).replace(/\. /g, '-').replace('.', '') : '-'}
- 규격 + 작업지시번호
-
W- x H-
+
{order.workOrderNo}
@@ -213,56 +296,43 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) {
No
품목명
-
출/부호
+
층/부호
규격
수량
상태
{/* 테이블 데이터 */} - {items.map((item, index) => ( -
-
{item.no}
-
{item.name}
-
{item.location}
-
{item.spec}
-
{item.qty}
-
{item.status}
+ {items.length > 0 ? ( + items.map((item, index) => ( +
+
{item.no}
+
{item.productName}
+
{item.floorCode}
+
{item.specification}
+
{item.quantity}
+
{ITEM_STATUS_LABELS[item.status]}
+
+ )) + ) : ( +
+ 등록된 품목이 없습니다.
- ))} + )}
{/* 작업내역 */}
{/* 검정 헤더 */}
- {PROCESS_LABELS[order.process]} 작업내역 + {order.processName} 작업내역
- {/* 작업내역 그리드 */} -
-
화단 유형
-
{workStats.workType}
-
화단 폭
-
{workStats.workWidth}
-
-
-
화단일반
-
{workStats.general} EA
-
이싱
-
{workStats.ironing} EA
-
-
-
센드락 작업
-
{workStats.sandblast} EA
-
포장
-
{workStats.packing} EA
-
{/* 수량 및 진행률 */} -
+
지시수량
{workStats.orderQty} EA
완료수량
@@ -270,6 +340,16 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) {
진행률
{workStats.progress}%
+ + {/* 상세 상태 */} +
+
대기
+
{workStats.waitingQty} EA
+
작업중
+
{workStats.inProgressQty} EA
+
완료
+
{workStats.completedQty} EA
+
{/* 특이 사항 */} @@ -278,7 +358,7 @@ export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) { 특이사항
- {order.instruction || '-'} + {order.note || '-'}
diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 704a2512..096e7ce1 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -327,7 +327,7 @@ export default function WorkerScreen() {