"use client"; import React, { useState, useMemo, useEffect, useCallback, useRef } 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 { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Plus, Trash2, RotateCcw, Save, X, ChevronDown, ChevronRight, ChevronLeft, Truck, Search, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { FormDatePicker } from "@/components/screen/filters/FormDatePicker"; import { getShippingOrderList, saveShippingOrder, deleteShippingOrders, previewShippingOrderNo, getShipmentPlanSource, getSalesOrderSource, getItemSource, } from "@/lib/api/shipping"; type DataSourceType = "shipmentPlan" | "salesOrder" | "itemInfo"; const STATUS_OPTIONS = [ { value: "all", label: "전체" }, { value: "READY", label: "준비중" }, { value: "IN_PROGRESS", label: "진행중" }, { value: "COMPLETED", label: "완료" }, ]; const getStatusLabel = (s: string) => STATUS_OPTIONS.find(o => o.value === s)?.label || s; const getStatusColor = (s: string) => { switch (s) { case "READY": return "bg-amber-100 text-amber-800 border-amber-200"; case "IN_PROGRESS": return "bg-blue-100 text-blue-800 border-blue-200"; case "COMPLETED": return "bg-emerald-100 text-emerald-800 border-emerald-200"; default: return "bg-gray-100 text-gray-800 border-gray-200"; } }; const getSourceBadge = (s: string) => { switch (s) { case "shipmentPlan": return { label: "출하계획", cls: "bg-blue-100 text-blue-700" }; case "salesOrder": return { label: "수주", cls: "bg-emerald-100 text-emerald-700" }; case "itemInfo": return { label: "품목", cls: "bg-purple-100 text-purple-700" }; default: return { label: s, cls: "bg-gray-100 text-gray-700" }; } }; interface SelectedItem { id: string | number; itemCode: string; itemName: string; spec: string; material: string; customer: string; planQty: number; orderQty: number; sourceType: DataSourceType; shipmentPlanId?: number; salesOrderId?: number; detailId?: string; partnerCode?: string; } export default function ShippingOrderPage() { const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(false); const [checkedIds, setCheckedIds] = useState([]); // 검색 const [searchKeyword, setSearchKeyword] = useState(""); const [searchCustomer, setSearchCustomer] = useState(""); const [debouncedKeyword, setDebouncedKeyword] = useState(""); const [debouncedCustomer, setDebouncedCustomer] = useState(""); const [searchStatus, setSearchStatus] = useState("all"); const [searchDateFrom, setSearchDateFrom] = useState(""); const [searchDateTo, setSearchDateTo] = useState(""); // 모달 const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [editId, setEditId] = useState(null); const [saving, setSaving] = useState(false); // 모달 폼 const [formOrderNumber, setFormOrderNumber] = useState(""); const [formOrderDate, setFormOrderDate] = useState(""); const [formCustomer, setFormCustomer] = useState(""); const [formPartnerId, setFormPartnerId] = useState(""); const [formStatus, setFormStatus] = useState("READY"); const [formCarrier, setFormCarrier] = useState(""); const [formVehicle, setFormVehicle] = useState(""); const [formDriver, setFormDriver] = useState(""); const [formDriverPhone, setFormDriverPhone] = useState(""); const [formArrival, setFormArrival] = useState(""); const [formAddress, setFormAddress] = useState(""); const [formMemo, setFormMemo] = useState(""); const [isTransportCollapsed, setIsTransportCollapsed] = useState(false); // 모달 왼쪽 패널 const [dataSource, setDataSource] = useState("shipmentPlan"); const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceData, setSourceData] = useState([]); const [sourceLoading, setSourceLoading] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const [sourcePage, setSourcePage] = useState(1); const [sourcePageSize] = useState(20); const [sourceTotalCount, setSourceTotalCount] = useState(0); // 텍스트 입력 debounce (500ms) useEffect(() => { const t = setTimeout(() => setDebouncedKeyword(searchKeyword), 500); return () => clearTimeout(t); }, [searchKeyword]); useEffect(() => { const t = setTimeout(() => setDebouncedCustomer(searchCustomer), 500); return () => clearTimeout(t); }, [searchCustomer]); // 초기 날짜 useEffect(() => { const today = new Date(); const from = new Date(today); from.setMonth(from.getMonth() - 1); setSearchDateFrom(from.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); }, []); // 데이터 조회 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 (debouncedCustomer.trim()) params.customer = debouncedCustomer.trim(); if (debouncedKeyword.trim()) params.keyword = debouncedKeyword.trim(); const result = await getShippingOrderList(params); if (result.success) setOrders(result.data || []); } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } }, [searchDateFrom, searchDateTo, searchStatus, debouncedCustomer, debouncedKeyword]); useEffect(() => { if (searchDateFrom && searchDateTo) fetchOrders(); }, [fetchOrders]); // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); try { const currentPage = pageOverride ?? sourcePage; const params: any = { page: currentPage, pageSize: sourcePageSize }; if (sourceKeyword.trim()) params.keyword = sourceKeyword.trim(); let result; switch (dataSource) { case "shipmentPlan": result = await getShipmentPlanSource(params); break; case "salesOrder": result = await getSalesOrderSource(params); break; case "itemInfo": result = await getItemSource(params); break; } if (result?.success) { setSourceData(result.data || []); setSourceTotalCount(result.totalCount || 0); } } catch (err) { console.error("소스 데이터 조회 실패:", err); } finally { setSourceLoading(false); } }, [dataSource, sourceKeyword, sourcePage, sourcePageSize]); useEffect(() => { if (isModalOpen) { setSourcePage(1); fetchSourceData(1); } }, [isModalOpen, dataSource]); // 핸들러 const handleResetSearch = () => { setSearchKeyword(""); setSearchCustomer(""); setDebouncedKeyword(""); setDebouncedCustomer(""); setSearchStatus("all"); const today = new Date(); const from = new Date(today); from.setMonth(from.getMonth() - 1); setSearchDateFrom(from.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); }; const handleCheckAll = (checked: boolean) => { setCheckedIds(checked ? orders.map((o: any) => o.id) : []); }; const handleDeleteSelected = async () => { if (checkedIds.length === 0) return; if (!confirm(`선택한 ${checkedIds.length}개의 출하지시를 삭제하시겠습니까?`)) return; try { const result = await deleteShippingOrders(checkedIds); if (result.success) { setCheckedIds([]); fetchOrders(); alert("삭제되었습니다."); } } catch (err: any) { alert(err.message || "삭제 실패"); } }; // 모달 열기 const openModal = (order?: any) => { if (order) { setIsEditMode(true); setEditId(order.id); setFormOrderNumber(order.instruction_no || ""); setFormOrderDate(order.instruction_date ? order.instruction_date.split("T")[0] : ""); setFormCustomer(order.customer_name || ""); setFormPartnerId(order.partner_id || ""); setFormStatus(order.status || "READY"); setFormCarrier(order.carrier_name || ""); setFormVehicle(order.vehicle_no || ""); setFormDriver(order.driver_name || ""); setFormDriverPhone(order.driver_contact || ""); setFormArrival(order.arrival_time ? order.arrival_time.slice(0, 16) : ""); setFormAddress(order.delivery_address || ""); setFormMemo(order.memo || ""); const items = order.items || []; setSelectedItems(items.filter((it: any) => it.id).map((it: any) => { const srcType = it.source_type || "shipmentPlan"; // 소스 데이터와 매칭할 수 있도록 원래 소스 id를 사용 let sourceId: string | number = it.id; if (srcType === "shipmentPlan" && it.shipment_plan_id) sourceId = it.shipment_plan_id; else if (srcType === "salesOrder" && it.detail_id) sourceId = it.detail_id; else if (srcType === "itemInfo") sourceId = it.item_code || ""; return { id: sourceId, itemCode: it.item_code || "", itemName: it.item_name || "", spec: it.spec || "", material: it.material || "", customer: order.customer_name || "", planQty: Number(it.plan_qty || 0), orderQty: Number(it.order_qty || 0), sourceType: srcType, shipmentPlanId: it.shipment_plan_id, salesOrderId: it.sales_order_id, detailId: it.detail_id, partnerCode: order.partner_id, }; })); } else { setIsEditMode(false); setEditId(null); setFormOrderNumber("불러오는 중..."); setFormOrderDate(new Date().toISOString().split("T")[0]); previewShippingOrderNo().then(r => { if (r.success) setFormOrderNumber(r.instructionNo); else setFormOrderNumber("(자동생성)"); }).catch(() => setFormOrderNumber("(자동생성)")); setFormCustomer(""); setFormPartnerId(""); setFormStatus("READY"); setFormCarrier(""); setFormVehicle(""); setFormDriver(""); setFormDriverPhone(""); setFormArrival(""); setFormAddress(""); setFormMemo(""); setSelectedItems([]); } setDataSource("shipmentPlan"); setSourceKeyword(""); setSourceData([]); setIsTransportCollapsed(false); setIsModalOpen(true); }; // 소스 아이템 선택 토글 const toggleSourceItem = (item: any) => { const key = dataSource === "shipmentPlan" ? item.id : dataSource === "salesOrder" ? item.id : item.item_code; const exists = selectedItems.findIndex(s => { // 같은 소스 타입에서 id 매칭 if (s.sourceType === dataSource) { if (dataSource === "itemInfo") return s.itemCode === key; return String(s.id) === String(key); } // 다른 소스 타입이라도 원래 소스 id로 매칭 if (dataSource === "shipmentPlan" && s.shipmentPlanId) return String(s.shipmentPlanId) === String(item.id); if (dataSource === "salesOrder" && s.detailId) return String(s.detailId) === String(item.id); return false; }); if (exists > -1) { setSelectedItems(prev => prev.filter((_, i) => i !== exists)); } else { const newItem: SelectedItem = { id: key, itemCode: item.item_code || "", itemName: item.item_name || "", spec: item.spec || "", material: item.material || "", customer: item.customer_name || "", planQty: Number(item.plan_qty || item.qty || item.balance_qty || 0), orderQty: Number(item.plan_qty || item.balance_qty || item.qty || 1), sourceType: dataSource, shipmentPlanId: dataSource === "shipmentPlan" ? item.id : undefined, salesOrderId: dataSource === "salesOrder" ? (item.master_id || undefined) : undefined, detailId: dataSource === "salesOrder" ? item.id : (dataSource === "shipmentPlan" ? item.detail_id : undefined), partnerCode: item.partner_code || "", }; setSelectedItems(prev => [...prev, newItem]); if (!formCustomer && item.customer_name) { setFormCustomer(item.customer_name); setFormPartnerId(item.partner_code || ""); } } }; const removeSelectedItem = (idx: number) => { setSelectedItems(prev => prev.filter((_, i) => i !== idx)); }; const updateOrderQty = (idx: number, val: number) => { setSelectedItems(prev => prev.map((item, i) => i === idx ? { ...item, orderQty: val } : item)); }; // 저장 const handleSave = async () => { if (!formOrderDate) { alert("출하지시일을 입력해주세요."); return; } if (selectedItems.length === 0) { alert("품목을 선택해주세요."); return; } setSaving(true); try { const payload = { id: isEditMode ? editId : undefined, instructionDate: formOrderDate, partnerId: formPartnerId || formCustomer, status: formStatus, memo: formMemo, carrierName: formCarrier, vehicleNo: formVehicle, driverName: formDriver, driverContact: formDriverPhone, arrivalTime: formArrival || null, deliveryAddress: formAddress, items: selectedItems.map(item => ({ itemCode: item.itemCode, itemName: item.itemName, spec: item.spec, material: item.material, orderQty: item.orderQty, planQty: item.planQty, shipQty: 0, sourceType: item.sourceType, shipmentPlanId: item.shipmentPlanId, salesOrderId: item.salesOrderId, detailId: item.detailId, })), }; const result = await saveShippingOrder(payload); if (result.success) { setIsModalOpen(false); fetchOrders(); alert(isEditMode ? "출하지시가 수정되었습니다." : "출하지시가 등록되었습니다."); } else { alert(result.message || "저장 실패"); } } catch (err: any) { alert(err.message || "저장 중 오류 발생"); } finally { setSaving(false); } }; const formatDate = (d: string) => d ? d.split("T")[0] : "-"; const dataSourceTitle: Record = { shipmentPlan: "출하계획 목록", salesOrder: "수주정보 목록", itemInfo: "품목정보 목록", }; return (
{/* 검색 */}
setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} />
setSearchCustomer(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} />
~
{loading && }
{/* 메인 테이블 */}
출하지시 관리 {orders.length}건
{loading ? (
) : ( 0 && checkedIds.length === orders.length} onCheckedChange={handleCheckAll} /> 출하지시번호 출하일자 거래처명 운송업체 차량번호 기사명 상태 품번 품명 수량 소스 비고 {orders.length === 0 ? (
등록된 출하지시가 없습니다
출하지시 등록 버튼으로 등록하세요
) : ( orders.map((order: any) => { const items = Array.isArray(order.items) ? order.items.filter((it: any) => it.id) : []; if (items.length === 0) { return ( openModal(order)}> e.stopPropagation()}> { if (c) setCheckedIds(p => [...p, order.id]); else setCheckedIds(p => p.filter(i => i !== order.id)); }} /> {order.instruction_no} {formatDate(order.instruction_date)} {order.customer_name || "-"} {order.carrier_name || "-"} {order.vehicle_no || "-"} {order.driver_name || "-"} {getStatusLabel(order.status)} - - 0 - {order.memo || "-"} ); } return items.map((item: any, itemIdx: number) => ( openModal(order)}> e.stopPropagation()}> {itemIdx === 0 && { if (c) setCheckedIds(p => [...p, order.id]); else setCheckedIds(p => p.filter(i => i !== order.id)); }} />} {itemIdx === 0 ? order.instruction_no : ""} {itemIdx === 0 ? formatDate(order.instruction_date) : ""} {itemIdx === 0 ? (order.customer_name || "-") : ""} {itemIdx === 0 ? (order.carrier_name || "-") : ""} {itemIdx === 0 ? (order.vehicle_no || "-") : ""} {itemIdx === 0 ? (order.driver_name || "-") : ""} {itemIdx === 0 && {getStatusLabel(order.status)}} {item.item_code} {item.item_name} {Number(item.order_qty || 0).toLocaleString()} {(() => { const b = getSourceBadge(item.source_type || ""); return {b.label}; })()} {itemIdx === 0 ? (order.memo || "-") : ""} )); }) )}
)}
{/* 등록/수정 모달 */} {isEditMode ? "출하지시 수정" : "출하지시 등록"} {isEditMode ? "출하지시 정보를 수정합니다." : "왼쪽에서 데이터를 선택하고 오른쪽에서 출하지시 정보를 입력하세요."}
{/* 왼쪽: 데이터 소스 */}
setSourceKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { setSourcePage(1); fetchSourceData(1); }}} />
{dataSourceTitle[dataSource]} 선택: {selectedItems.length}
{sourceLoading ? (
) : sourceData.length === 0 ? (
조회 버튼을 눌러 데이터를 불러오세요
) : ( 선택 품번 품명 규격 거래처 수량 {dataSource === "shipmentPlan" && 상태} {sourceData.map((item: any, idx: number) => { const itemId = dataSource === "itemInfo" ? item.item_code : item.id; const isSelected = selectedItems.some(s => { // 같은 소스 타입에서 id 매칭 if (s.sourceType === dataSource) { if (dataSource === "itemInfo") return s.itemCode === itemId; return String(s.id) === String(itemId); } // 다른 소스 타입이라도 같은 품번이면 중복 방지 if (dataSource === "shipmentPlan" && s.shipmentPlanId) return String(s.shipmentPlanId) === String(item.id); if (dataSource === "salesOrder" && s.detailId) return String(s.detailId) === String(item.id); return false; }); return ( toggleSourceItem(item)}> e.stopPropagation()}> toggleSourceItem(item)} /> {item.item_code || "-"} {item.item_name || "-"} {item.spec || "-"} {item.customer_name || "-"} {Number(item.plan_qty || item.qty || item.balance_qty || 0).toLocaleString()} {dataSource === "shipmentPlan" && ( {getStatusLabel(item.status)} )} ); })}
)}
{/* 페이징 */} {sourceTotalCount > 0 && (
총 {sourceTotalCount}건 중 {(sourcePage - 1) * sourcePageSize + 1}-{Math.min(sourcePage * sourcePageSize, sourceTotalCount)}건
{sourcePage} / {Math.max(1, Math.ceil(sourceTotalCount / sourcePageSize))}
)}
{/* 오른쪽: 폼 */}
{/* 기본 정보 */}

기본 정보

{/* 운송 정보 */}
{!isTransportCollapsed && (
setFormCarrier(e.target.value)} className="h-9" />
setFormVehicle(e.target.value)} className="h-9" />
setFormDriver(e.target.value)} className="h-9" />
setFormDriverPhone(e.target.value)} className="h-9" />
setFormAddress(e.target.value)} className="h-9" />
)}
{/* 선택된 품목 */}

선택된 품목 {selectedItems.length}

{selectedItems.length === 0 ? (
왼쪽에서 데이터를 선택하세요
) : ( 소스 품번 품명 출하수량 계획수량 삭제 {selectedItems.map((item, idx) => { const b = getSourceBadge(item.sourceType); return ( {b.label.charAt(0)} {item.itemCode} {item.itemName} updateOrderQty(idx, parseInt(e.target.value) || 0)} min={1} className="h-7 w-[70px] text-xs text-right mx-auto" /> {item.planQty ? item.planQty.toLocaleString() : "-"} ); })}
)}
{/* 메모 */}

메모