"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Plus, Trash2, RotateCcw, Save, X, ChevronLeft, ChevronRight, Search, Loader2, Wrench, Pencil, CheckCircle2, ArrowRight, Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { FormDatePicker } from "@/components/screen/filters/FormDatePicker"; import { getWorkInstructionList, previewWorkInstructionNo, saveWorkInstruction, deleteWorkInstructions, getWIItemSource, getWISalesOrderSource, getWIProductionPlanSource, getEquipmentList, getEmployeeList, } from "@/lib/api/workInstruction"; type SourceType = "production" | "order" | "item"; const STATUS_BADGE: Record = { "일반": { label: "일반", cls: "bg-blue-100 text-blue-800 border-blue-200" }, "긴급": { label: "긴급", cls: "bg-red-100 text-red-800 border-red-200" }, }; const PROGRESS_BADGE: Record = { "대기": { label: "대기", cls: "bg-amber-100 text-amber-800" }, "진행중": { label: "진행중", cls: "bg-blue-100 text-blue-800" }, "완료": { label: "완료", cls: "bg-emerald-100 text-emerald-800" }, }; interface EquipmentOption { id: string; equipment_code: string; equipment_name: string; } interface EmployeeOption { user_id: string; user_name: string; dept_name: string | null; } interface SelectedItem { itemCode: string; itemName: string; spec: string; qty: number; remark: string; sourceType: SourceType; sourceTable: string; sourceId: string | number; } export default function WorkInstructionPage() { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(false); const [equipmentOptions, setEquipmentOptions] = useState([]); const [employeeOptions, setEmployeeOptions] = useState([]); // 검색 const [searchKeyword, setSearchKeyword] = useState(""); const [debouncedKeyword, setDebouncedKeyword] = useState(""); const [searchStatus, setSearchStatus] = useState("all"); const [searchProgress, setSearchProgress] = useState("all"); const [searchDateFrom, setSearchDateFrom] = useState(""); const [searchDateTo, setSearchDateTo] = useState(""); // 1단계: 등록 모달 const [isRegModalOpen, setIsRegModalOpen] = useState(false); const [regSourceType, setRegSourceType] = useState(""); const [regSourceData, setRegSourceData] = useState([]); const [regSourceLoading, setRegSourceLoading] = useState(false); const [regKeyword, setRegKeyword] = useState(""); const [regCheckedIds, setRegCheckedIds] = useState>(new Set()); const [regMergeSameItem, setRegMergeSameItem] = useState(true); const [regPage, setRegPage] = useState(1); const [regPageSize] = useState(20); const [regTotalCount, setRegTotalCount] = useState(0); // 2단계: 확인 모달 const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const [confirmItems, setConfirmItems] = useState([]); const [confirmWiNo, setConfirmWiNo] = useState(""); const [confirmStatus, setConfirmStatus] = useState("일반"); const [confirmStartDate, setConfirmStartDate] = useState(""); const [confirmEndDate, setConfirmEndDate] = useState(""); const nv = (v: string) => v || "none"; const fromNv = (v: string) => v === "none" ? "" : v; const [confirmEquipmentId, setConfirmEquipmentId] = useState(""); const [confirmWorkTeam, setConfirmWorkTeam] = useState(""); const [confirmWorker, setConfirmWorker] = useState(""); const [saving, setSaving] = useState(false); // 수정 모달 const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [editOrder, setEditOrder] = useState(null); const [editItems, setEditItems] = useState([]); const [editStatus, setEditStatus] = useState("일반"); const [editStartDate, setEditStartDate] = useState(""); const [editEndDate, setEditEndDate] = useState(""); const [editEquipmentId, setEditEquipmentId] = useState(""); const [editWorkTeam, setEditWorkTeam] = useState(""); const [editWorker, setEditWorker] = useState(""); const [editRemark, setEditRemark] = useState(""); const [editSaving, setEditSaving] = useState(false); const [addQty, setAddQty] = useState(""); const [addEquipment, setAddEquipment] = useState(""); const [addWorkTeam, setAddWorkTeam] = useState(""); const [addWorker, setAddWorker] = useState(""); const [confirmWorkerOpen, setConfirmWorkerOpen] = useState(false); const [editWorkerOpen, setEditWorkerOpen] = useState(false); const [addWorkerOpen, setAddWorkerOpen] = useState(false); useEffect(() => { const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500); return () => clearTimeout(t); }, [searchKeyword]); useEffect(() => { getEquipmentList().then(r => { if (r.success) setEquipmentOptions(r.data || []); }); getEmployeeList().then(r => { if (r.success) setEmployeeOptions(r.data || []); }); }, []); const fetchOrders = useCallback(async () => { setLoading(true); try { const params: any = {}; if (searchDateFrom) params.dateFrom = searchDateFrom; if (searchDateTo) params.dateTo = searchDateTo; if (searchStatus !== "all") params.status = searchStatus; if (searchProgress !== "all") params.progressStatus = searchProgress; if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim(); const r = await getWorkInstructionList(params); if (r.success) setOrders(r.data || []); } catch {} finally { setLoading(false); } }, [searchDateFrom, searchDateTo, searchStatus, searchProgress, debouncedKeyword]); useEffect(() => { fetchOrders(); }, [fetchOrders]); const handleResetSearch = () => { setSearchKeyword(""); setDebouncedKeyword(""); setSearchStatus("all"); setSearchProgress("all"); setSearchDateFrom(""); setSearchDateTo(""); }; // ─── 1단계 등록 ─── const openRegModal = () => { setRegSourceType("production"); setRegSourceData([]); setRegKeyword(""); setRegCheckedIds(new Set()); setRegPage(1); setRegTotalCount(0); setRegMergeSameItem(true); setIsRegModalOpen(true); }; const fetchRegSource = useCallback(async (pageOverride?: number) => { if (!regSourceType) return; setRegSourceLoading(true); try { const p = pageOverride ?? regPage; const params: any = { page: p, pageSize: regPageSize }; if (regKeyword.trim()) params.keyword = regKeyword.trim(); let r; switch (regSourceType) { case "production": r = await getWIProductionPlanSource(params); break; case "order": r = await getWISalesOrderSource(params); break; case "item": r = await getWIItemSource(params); break; } if (r?.success) { setRegSourceData(r.data || []); setRegTotalCount(r.totalCount || 0); } } catch {} finally { setRegSourceLoading(false); } }, [regSourceType, regKeyword, regPage, regPageSize]); useEffect(() => { if (isRegModalOpen && regSourceType) { setRegPage(1); setRegCheckedIds(new Set()); fetchRegSource(1); } }, [regSourceType]); const getRegId = (item: any) => regSourceType === "item" ? (item.item_code || item.id) : String(item.id); const toggleRegItem = (id: string) => { setRegCheckedIds(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); }; const toggleRegAll = () => { if (regCheckedIds.size === regSourceData.length) setRegCheckedIds(new Set()); else setRegCheckedIds(new Set(regSourceData.map(getRegId))); }; const applyRegistration = () => { if (regCheckedIds.size === 0) { alert("품목을 선택해주세요."); return; } const items: SelectedItem[] = []; for (const item of regSourceData) { if (!regCheckedIds.has(getRegId(item))) continue; if (regSourceType === "item") items.push({ itemCode: item.item_code, itemName: item.item_name || "", spec: item.spec || "", qty: 1, remark: "", sourceType: "item", sourceTable: "item_info", sourceId: item.item_code }); else if (regSourceType === "order") items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", qty: Number(item.qty || 1), remark: "", sourceType: "order", sourceTable: "sales_order_detail", sourceId: item.id }); else items.push({ itemCode: item.item_code || "", itemName: item.item_name || "", spec: "", qty: Number(item.plan_qty || 1), remark: "", sourceType: "production", sourceTable: "production_plan_mng", sourceId: item.id }); } // 동일품목 합산 if (regMergeSameItem) { const merged = new Map(); for (const it of items) { const key = it.itemCode; if (merged.has(key)) { merged.get(key)!.qty += it.qty; } else { merged.set(key, { ...it }); } } setConfirmItems(Array.from(merged.values())); } else { setConfirmItems(items); } setConfirmWiNo("불러오는 중..."); setConfirmStatus("일반"); setConfirmStartDate(new Date().toISOString().split("T")[0]); setConfirmEndDate(""); setConfirmEquipmentId(""); setConfirmWorkTeam(""); setConfirmWorker(""); previewWorkInstructionNo().then(r => { if (r.success) setConfirmWiNo(r.instructionNo); else setConfirmWiNo("(자동생성)"); }).catch(() => setConfirmWiNo("(자동생성)")); setIsRegModalOpen(false); setIsConfirmModalOpen(true); }; // ─── 2단계 최종 적용 ─── const finalizeRegistration = async () => { if (confirmItems.length === 0) { alert("품목이 없습니다."); return; } setSaving(true); try { const payload = { status: confirmStatus, startDate: confirmStartDate, endDate: confirmEndDate, equipmentId: confirmEquipmentId, workTeam: confirmWorkTeam, worker: confirmWorker, items: confirmItems.map(i => ({ itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsConfirmModalOpen(false); fetchOrders(); alert("작업지시가 등록되었습니다."); } else alert(r.message || "저장 실패"); } catch (e: any) { alert(e.message || "저장 실패"); } finally { setSaving(false); } }; // ─── 수정 모달 ─── const openEditModal = (order: any) => { const wiNo = order.work_instruction_no; const relatedDetails = orders.filter(o => o.work_instruction_no === wiNo); setEditOrder(order); setEditStatus(order.status || "일반"); setEditStartDate(order.start_date || ""); setEditEndDate(order.end_date || ""); setEditEquipmentId(order.equipment_id || ""); setEditWorkTeam(order.work_team || ""); setEditWorker(order.worker || ""); setEditRemark(order.wi_remark || ""); setEditItems(relatedDetails.map((d: any) => ({ itemCode: d.item_number || d.part_code || "", itemName: d.item_name || "", spec: d.item_spec || "", qty: Number(d.detail_qty || 0), remark: d.detail_remark || "", sourceType: (d.source_table === "sales_order_detail" ? "order" : d.source_table === "production_plan_mng" ? "production" : "item") as SourceType, sourceTable: d.source_table || "item_info", sourceId: d.source_id || "", }))); setAddQty(""); setAddEquipment(""); setAddWorkTeam(""); setAddWorker(""); setIsEditModalOpen(true); }; const addEditItem = () => { if (!addQty || Number(addQty) <= 0) { alert("수량을 입력해주세요."); return; } setEditItems(prev => [...prev, { itemCode: editOrder?.item_number || "", itemName: editOrder?.item_name || "", spec: editOrder?.item_spec || "", qty: Number(addQty), remark: "", sourceType: "item", sourceTable: "item_info", sourceId: editOrder?.item_number || "", }]); setAddQty(""); }; const saveEdit = async () => { if (!editOrder || editItems.length === 0) { alert("품목이 없습니다."); return; } setEditSaving(true); try { const payload = { id: editOrder.wi_id, status: editStatus, startDate: editStartDate, endDate: editEndDate, equipmentId: editEquipmentId, workTeam: editWorkTeam, worker: editWorker, remark: editRemark, items: editItems.map(i => ({ itemNumber: i.itemCode, itemCode: i.itemCode, qty: String(i.qty), remark: i.remark, sourceTable: i.sourceTable, sourceId: String(i.sourceId), partCode: i.itemCode })), }; const r = await saveWorkInstruction(payload); if (r.success) { setIsEditModalOpen(false); fetchOrders(); alert("수정되었습니다."); } else alert(r.message || "저장 실패"); } catch (e: any) { alert(e.message || "저장 실패"); } finally { setEditSaving(false); } }; const handleDelete = async (wiId: string) => { if (!confirm("이 작업지시를 삭제하시겠습니까?")) return; const r = await deleteWorkInstructions([wiId]); if (r.success) { fetchOrders(); } else alert(r.message || "삭제 실패"); }; const getProgress = (o: any) => { const t = Number(o.total_qty || 0), c = Number(o.completed_qty || 0); return t === 0 ? 0 : Math.min(100, Math.round((c / t) * 100)); }; const getProgressLabel = (o: any) => { const p = getProgress(o); if (o.progress_status) return o.progress_status; if (p >= 100) return "완료"; if (p > 0) return "진행중"; return "대기"; }; const totalRegPages = Math.max(1, Math.ceil(regTotalCount / regPageSize)); const getDisplayNo = (o: any) => { const cnt = Number(o.detail_count || 1); const seq = Number(o.detail_seq || 1); if (cnt <= 1) return o.work_instruction_no || "-"; return `${o.work_instruction_no}-${String(seq).padStart(2, "0")}`; }; const getWorkerName = (userId: string) => { if (!userId) return "-"; const emp = employeeOptions.find(e => e.user_id === userId); return emp ? emp.user_name : userId; }; const WorkerCombobox = ({ value, onChange, open, onOpenChange, className, triggerClassName }: { value: string; onChange: (v: string) => void; open: boolean; onOpenChange: (v: boolean) => void; className?: string; triggerClassName?: string; }) => ( 사원을 찾을 수 없습니다 { onChange(""); onOpenChange(false); }} className="text-xs"> 선택 안 함 {employeeOptions.map(emp => ( { onChange(emp.user_id); onOpenChange(false); }} className="text-xs"> {emp.user_name}{emp.dept_name ? ` (${emp.dept_name})` : ""} ))} ); return (
{/* 검색 */}
~
setSearchKeyword(e.target.value)} className="h-9 w-[200px]" />
{loading && }
{/* 메인 테이블 */}

작업지시 목록 {new Set(orders.map(o => o.work_instruction_no)).size}건 ({orders.length}행)

작업지시번호 상태 진행현황 품목명 규격 수량 설비 작업조 작업자 시작일 완료일 작업 {loading ? ( ) : orders.length === 0 ? ( 작업지시가 없습니다 ) : orders.map((o, rowIdx) => { const pct = getProgress(o); const pLabel = getProgressLabel(o); const pBadge = PROGRESS_BADGE[pLabel] || PROGRESS_BADGE["대기"]; const sBadge = STATUS_BADGE[o.status] || STATUS_BADGE["일반"]; const isFirstOfGroup = Number(o.detail_seq) === 1; return ( {getDisplayNo(o)} {sBadge.label} {isFirstOfGroup ? (
{pBadge.label}
= 100 ? "bg-emerald-500" : pct > 0 ? "bg-blue-500" : "bg-gray-300")} style={{ width: `${pct}%` }} />
{pct}%
) : } {o.item_name || o.item_number || "-"} {o.item_spec || "-"} {Number(o.detail_qty || 0).toLocaleString()} {isFirstOfGroup ? (o.equipment_name || "-") : ""} {isFirstOfGroup ? (o.work_team || "-") : ""} {isFirstOfGroup ? getWorkerName(o.worker) : ""} {isFirstOfGroup ? (o.start_date || "-") : ""} {isFirstOfGroup ? (o.end_date || "-") : ""} {isFirstOfGroup && (
)}
); })}
{/* ── 1단계: 등록 모달 ── */} 작업지시 등록 근거를 선택하고 품목을 체크한 후 "작업지시 적용" 버튼을 눌러주세요.
{regSourceType && (<> setRegKeyword(e.target.value)} className="h-9 w-[220px]" onKeyDown={e => { if (e.key === "Enter") { setRegPage(1); fetchRegSource(1); } }} /> )}
{!regSourceType ? (
근거를 선택하고 검색해주세요
) : regSourceLoading ? (
) : regSourceData.length === 0 ? (
데이터가 없습니다
) : ( 0 && regCheckedIds.size === regSourceData.length} onCheckedChange={toggleRegAll} /> {regSourceType === "item" && <>품목코드품목명규격} {regSourceType === "order" && <>수주번호품번품목명규격수량납기일} {regSourceType === "production" && <>계획번호품번품목명계획수량시작일완료일설비} {regSourceData.map((item, idx) => { const id = getRegId(item); const checked = regCheckedIds.has(id); return ( toggleRegItem(id)}> e.stopPropagation()}> toggleRegItem(id)} /> {regSourceType === "item" && <>{item.item_code}{item.item_name}{item.spec || "-"}} {regSourceType === "order" && <>{item.order_no}{item.item_code}{item.item_name}{item.spec || "-"}{Number(item.qty || 0).toLocaleString()}{item.due_date || "-"}} {regSourceType === "production" && <>{item.plan_no}{item.item_code}{item.item_name}{Number(item.plan_qty || 0).toLocaleString()}{item.start_date ? String(item.start_date).split("T")[0] : "-"}{item.end_date ? String(item.end_date).split("T")[0] : "-"}{item.equipment_name || "-"}} ); })}
)}
{regTotalCount > 0 && (
총 {regTotalCount}건 (선택: {regCheckedIds.size}건)
{regPage} / {totalRegPages}
)}
{/* ── 2단계: 확인 모달 ── */} 작업지시 적용 확인 기본 정보를 입력하고 "최종 적용" 버튼을 눌러주세요.

작업지시 기본 정보

품목 목록

순번품목코드품목명규격수량비고 {confirmItems.map((item, idx) => ( {idx + 1} {item.itemCode} {item.itemName || item.itemCode} {item.spec || "-"} setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> setConfirmItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> ))}
{/* ── 수정 모달 ── */} 작업지시 관리 - {editOrder?.work_instruction_no} 품목을 추가/삭제하고 정보를 수정하세요.

기본 정보

setEditRemark(e.target.value)} className="h-9" placeholder="비고" />
{/* 인라인 추가 폼 */}
setAddQty(e.target.value)} className="h-8 w-24 text-xs" placeholder="0" />
{/* 품목 테이블 */}
작업지시 항목 {editItems.length}건
순번품목코드수량비고 {editItems.length === 0 ? ( 품목이 없습니다 ) : editItems.map((item, idx) => ( {idx + 1} {item.itemCode} setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, qty: Number(e.target.value) } : it))} /> setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, remark: e.target.value } : it))} /> ))}
{editItems.length > 0 && (
총 수량 {editItems.reduce((s, i) => s + i.qty, 0).toLocaleString()} EA
)}
); }