"use client"; import React, { useState, useEffect, useCallback, useMemo } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { Search, Plus, Trash2, RotateCcw, Loader2, Package, X, Save, ChevronRight, } from "lucide-react"; import { cn } from "@/lib/utils"; import { getReceivingList, createReceiving, deleteReceiving, generateReceivingNumber, getReceivingWarehouses, getPurchaseOrderSources, getShipmentSources, getItemSources, type InboundItem, type PurchaseOrderSource, type ShipmentSource, type ItemSource, type WarehouseOption, } from "@/lib/api/receiving"; // 입고유형 옵션 const INBOUND_TYPES = [ { value: "구매입고", label: "구매입고", color: "bg-blue-100 text-blue-800" }, { value: "반품입고", label: "반품입고", color: "bg-pink-100 text-pink-800" }, { value: "기타입고", label: "기타입고", color: "bg-gray-100 text-gray-800" }, ]; const INBOUND_STATUS_OPTIONS = [ { value: "대기", label: "대기", color: "bg-amber-100 text-amber-800" }, { value: "입고완료", label: "입고완료", color: "bg-emerald-100 text-emerald-800" }, { value: "부분입고", label: "부분입고", color: "bg-amber-100 text-amber-800" }, { value: "입고취소", label: "입고취소", color: "bg-red-100 text-red-800" }, ]; const getTypeColor = (type: string) => INBOUND_TYPES.find((t) => t.value === type)?.color || "bg-gray-100 text-gray-800"; const getStatusColor = (status: string) => INBOUND_STATUS_OPTIONS.find((s) => s.value === status)?.color || "bg-gray-100 text-gray-800"; // 소스 테이블 한글명 매핑 const SOURCE_TABLE_LABEL: Record = { purchase_order_mng: "발주", shipment_instruction_detail: "출하", item_info: "품목", }; // 선택된 소스 아이템 (등록 모달에서 사용) interface SelectedSourceItem { key: string; inbound_type: string; reference_number: string; supplier_code: string; supplier_name: string; item_number: string; item_name: string; spec: string; material: string; unit: string; inbound_qty: number; unit_price: number; total_amount: number; source_table: string; source_id: string; } export default function ReceivingPage() { // 목록 데이터 const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [checkedIds, setCheckedIds] = useState([]); // 검색 필터 const [searchType, setSearchType] = useState("all"); const [searchStatus, setSearchStatus] = useState("all"); const [searchKeyword, setSearchKeyword] = useState(""); const [searchDateFrom, setSearchDateFrom] = useState(""); const [searchDateTo, setSearchDateTo] = useState(""); // 등록 모달 const [isModalOpen, setIsModalOpen] = useState(false); const [modalInboundType, setModalInboundType] = useState("구매입고"); const [modalInboundNo, setModalInboundNo] = useState(""); const [modalInboundDate, setModalInboundDate] = useState(""); const [modalWarehouse, setModalWarehouse] = useState(""); const [modalLocation, setModalLocation] = useState(""); const [modalInspector, setModalInspector] = useState(""); const [modalManager, setModalManager] = useState(""); const [modalMemo, setModalMemo] = useState(""); const [selectedItems, setSelectedItems] = useState([]); const [saving, setSaving] = useState(false); // 소스 데이터 const [sourceKeyword, setSourceKeyword] = useState(""); const [sourceLoading, setSourceLoading] = useState(false); const [purchaseOrders, setPurchaseOrders] = useState([]); const [shipments, setShipments] = useState([]); const [items, setItems] = useState([]); const [warehouses, setWarehouses] = useState([]); // 날짜 초기화 useEffect(() => { const today = new Date(); const threeMonthsAgo = new Date(today); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); }, []); // 목록 조회 const fetchList = useCallback(async () => { setLoading(true); try { const res = await getReceivingList({ inbound_type: searchType !== "all" ? searchType : undefined, inbound_status: searchStatus !== "all" ? searchStatus : undefined, search_keyword: searchKeyword || undefined, date_from: searchDateFrom || undefined, date_to: searchDateTo || undefined, }); if (res.success) setData(res.data); } catch { // 에러 무시 } finally { setLoading(false); } }, [searchType, searchStatus, searchKeyword, searchDateFrom, searchDateTo]); useEffect(() => { fetchList(); }, [fetchList]); // 창고 목록 로드 useEffect(() => { (async () => { try { const res = await getReceivingWarehouses(); if (res.success) setWarehouses(res.data); } catch { // ignore } })(); }, []); // 검색 초기화 const handleReset = () => { setSearchType("all"); setSearchStatus("all"); setSearchKeyword(""); const today = new Date(); const threeMonthsAgo = new Date(today); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); setSearchDateFrom(threeMonthsAgo.toISOString().split("T")[0]); setSearchDateTo(today.toISOString().split("T")[0]); }; // 체크박스 const allChecked = data.length > 0 && checkedIds.length === data.length; const toggleCheckAll = () => { setCheckedIds(allChecked ? [] : data.map((d) => d.id)); }; const toggleCheck = (id: string) => { setCheckedIds((prev) => prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] ); }; // 삭제 const handleDelete = async () => { if (checkedIds.length === 0) return; if (!confirm(`선택한 ${checkedIds.length}건을 삭제하시겠습니까?`)) return; for (const id of checkedIds) { await deleteReceiving(id); } setCheckedIds([]); fetchList(); }; // --- 등록 모달 --- // 소스 데이터 로드 함수 const loadSourceData = useCallback( async (type: string, keyword?: string) => { setSourceLoading(true); try { if (type === "구매입고") { const res = await getPurchaseOrderSources(keyword || undefined); if (res.success) setPurchaseOrders(res.data); } else if (type === "반품입고") { const res = await getShipmentSources(keyword || undefined); if (res.success) setShipments(res.data); } else { const res = await getItemSources(keyword || undefined); if (res.success) setItems(res.data); } } catch { // ignore } finally { setSourceLoading(false); } }, [] ); const openRegisterModal = async () => { const defaultType = "구매입고"; setModalInboundType(defaultType); setModalInboundDate(new Date().toISOString().split("T")[0]); setModalWarehouse(""); setModalLocation(""); setModalInspector(""); setModalManager(""); setModalMemo(""); setSelectedItems([]); setSourceKeyword(""); setPurchaseOrders([]); setShipments([]); setItems([]); setIsModalOpen(true); // 입고번호 생성 + 발주 데이터 동시 로드 try { const [numRes] = await Promise.all([ generateReceivingNumber(), loadSourceData(defaultType), ]); if (numRes.success) setModalInboundNo(numRes.data); } catch { setModalInboundNo(""); } }; // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { await loadSourceData(modalInboundType, sourceKeyword || undefined); }, [modalInboundType, sourceKeyword, loadSourceData]); // 입고유형 변경 시 소스 데이터 자동 리로드 const handleInboundTypeChange = useCallback( (type: string) => { setModalInboundType(type); setSourceKeyword(""); setPurchaseOrders([]); setShipments([]); setItems([]); setSelectedItems([]); loadSourceData(type); }, [loadSourceData] ); // 발주 품목 추가 const addPurchaseOrder = (po: PurchaseOrderSource) => { const key = `po-${po.id}`; if (selectedItems.some((s) => s.key === key)) return; setSelectedItems((prev) => [ ...prev, { key, inbound_type: "구매입고", reference_number: po.purchase_no, supplier_code: po.supplier_code, supplier_name: po.supplier_name, item_number: po.item_code, item_name: po.item_name, spec: po.spec || "", material: po.material || "", unit: "EA", inbound_qty: po.remain_qty, unit_price: po.unit_price, total_amount: po.remain_qty * po.unit_price, source_table: "purchase_order_mng", source_id: po.id, }, ]); }; // 출하 품목 추가 const addShipment = (sh: ShipmentSource) => { const key = `sh-${sh.detail_id}`; if (selectedItems.some((s) => s.key === key)) return; setSelectedItems((prev) => [ ...prev, { key, inbound_type: "반품입고", reference_number: sh.instruction_no, supplier_code: "", supplier_name: sh.partner_id, item_number: sh.item_code, item_name: sh.item_name, spec: sh.spec || "", material: sh.material || "", unit: "EA", inbound_qty: sh.ship_qty, unit_price: 0, total_amount: 0, source_table: "shipment_instruction_detail", source_id: String(sh.detail_id), }, ]); }; // 품목 추가 const addItem = (item: ItemSource) => { const key = `item-${item.id}`; if (selectedItems.some((s) => s.key === key)) return; setSelectedItems((prev) => [ ...prev, { key, inbound_type: "기타입고", reference_number: item.item_number, supplier_code: "", supplier_name: "", item_number: item.item_number, item_name: item.item_name, spec: item.spec || "", material: item.material || "", unit: item.unit || "EA", inbound_qty: 0, unit_price: item.standard_price, total_amount: 0, source_table: "item_info", source_id: item.id, }, ]); }; // 선택 품목 수량 변경 const updateItemQty = (key: string, qty: number) => { setSelectedItems((prev) => prev.map((item) => item.key === key ? { ...item, inbound_qty: qty, total_amount: qty * item.unit_price } : item ) ); }; // 선택 품목 단가 변경 const updateItemPrice = (key: string, price: number) => { setSelectedItems((prev) => prev.map((item) => item.key === key ? { ...item, unit_price: price, total_amount: item.inbound_qty * price } : item ) ); }; // 선택 품목 삭제 const removeItem = (key: string) => { setSelectedItems((prev) => prev.filter((item) => item.key !== key)); }; // 저장 const handleSave = async () => { if (selectedItems.length === 0) { alert("입고할 품목을 선택해주세요."); return; } if (!modalInboundDate) { alert("입고일을 입력해주세요."); return; } const zeroQtyItems = selectedItems.filter((i) => !i.inbound_qty || i.inbound_qty <= 0); if (zeroQtyItems.length > 0) { alert("입고수량이 0인 품목이 있습니다. 수량을 입력해주세요."); return; } setSaving(true); try { const res = await createReceiving({ inbound_number: modalInboundNo, inbound_date: modalInboundDate, warehouse_code: modalWarehouse || undefined, location_code: modalLocation || undefined, inspector: modalInspector || undefined, manager: modalManager || undefined, memo: modalMemo || undefined, items: selectedItems.map((item) => ({ inbound_type: item.inbound_type, reference_number: item.reference_number, supplier_code: item.supplier_code, supplier_name: item.supplier_name, item_number: item.item_number, item_name: item.item_name, spec: item.spec, material: item.material, unit: item.unit, inbound_qty: item.inbound_qty, unit_price: item.unit_price, total_amount: item.total_amount, source_table: item.source_table, source_id: item.source_id, inbound_status: "입고완료", inspection_status: "대기", })), }); if (res.success) { alert(res.message || "입고 등록 완료"); setIsModalOpen(false); fetchList(); } } catch { alert("입고 등록 중 오류가 발생했습니다."); } finally { setSaving(false); } }; // 합계 계산 const totalSummary = useMemo(() => { return { count: selectedItems.length, qty: selectedItems.reduce((sum, i) => sum + (i.inbound_qty || 0), 0), amount: selectedItems.reduce((sum, i) => sum + (i.total_amount || 0), 0), }; }, [selectedItems]); return (
{/* 검색 영역 */}
setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && fetchList()} className="h-9 w-[240px] text-xs" />
setSearchDateFrom(e.target.value)} className="h-9 w-[140px] text-xs" /> ~ setSearchDateTo(e.target.value)} className="h-9 w-[140px] text-xs" />
{/* 입고 목록 테이블 */}

입고 목록

총 {data.length}건
입고번호 입고유형 입고일 참조번호 데이터출처 공급처 품목코드 품목명 규격 입고수량 단가 금액 창고 입고상태 비고 {loading ? ( ) : data.length === 0 ? (

등록된 입고 내역이 없습니다

'입고 등록' 버튼을 클릭하여 입고를 추가하세요

) : ( data.map((row) => ( toggleCheck(row.id)} > e.stopPropagation()} > toggleCheck(row.id)} /> {row.inbound_number} {row.inbound_type || "-"} {row.inbound_date ? new Date(row.inbound_date).toLocaleDateString("ko-KR") : "-"} {row.reference_number || "-"} {row.source_table ? SOURCE_TABLE_LABEL[row.source_table] || row.source_table : "-"} {row.supplier_name || "-"} {row.item_number || "-"} {row.item_name || "-"} {row.spec || "-"} {Number(row.inbound_qty || 0).toLocaleString()} {Number(row.unit_price || 0).toLocaleString()} {Number(row.total_amount || 0).toLocaleString()} {row.warehouse_name || row.warehouse_code || "-"} {row.inbound_status || "-"} {row.memo || "-"} )) )}
{/* 입고 등록 모달 */} 입고 등록 입고유형을 선택하고, 좌측에서 근거 데이터를 검색하여 추가하세요. {/* 입고유형 선택 */}
{modalInboundType === "구매입고" ? "발주 데이터에서 입고 처리합니다." : modalInboundType === "반품입고" ? "출하 데이터에서 반품 입고 처리합니다." : "품목 데이터를 직접 선택하여 입고 처리합니다."}
{/* 메인 콘텐츠: 좌측 소스 데이터 / 우측 선택 품목 */}
{/* 좌측: 근거 데이터 검색 */}
{/* 소스 검색 바 */}
setSourceKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && searchSourceData()} className="h-8 flex-1 text-xs" />
{/* 소스 데이터 테이블 */}

{modalInboundType === "구매입고" ? "미입고 발주 목록" : modalInboundType === "반품입고" ? "출하 목록" : "품목 목록"}

{sourceLoading ? (
) : modalInboundType === "구매입고" ? ( s.key)} /> ) : modalInboundType === "반품입고" ? ( s.key)} /> ) : ( s.key)} /> )}
{/* 우측: 입고 정보 + 선택 품목 */}
{/* 입고 정보 입력 */}

입고 정보

setModalInboundDate(e.target.value)} className="h-8 text-xs" />
setModalLocation(e.target.value)} placeholder="위치 입력" className="h-8 text-xs" />
setModalInspector(e.target.value)} placeholder="검수자" className="h-8 text-xs" />
setModalManager(e.target.value)} placeholder="담당자" className="h-8 text-xs" />
setModalMemo(e.target.value)} placeholder="메모" className="h-8 text-xs" />
{/* 선택된 품목 테이블 */}

입고 처리 품목 ({selectedItems.length}건)

{selectedItems.length === 0 ? (
좌측에서 품목을 선택하여 추가해주세요
) : ( No 품목명 참조번호 수량 단가 금액 {selectedItems.map((item, idx) => ( {idx + 1}
{item.item_name} {item.item_number} {item.spec ? ` | ${item.spec}` : ""}
{item.reference_number} updateItemQty( item.key, Number(e.target.value) || 0 ) } className="h-7 w-[70px] text-right text-xs" min={0} /> updateItemPrice( item.key, Number(e.target.value) || 0 ) } className="h-7 w-[70px] text-right text-xs" min={0} /> {item.total_amount.toLocaleString()}
))}
)}
{/* 푸터 */}
{selectedItems.length > 0 ? ( <> {totalSummary.count}건 | 수량 합계:{" "} {totalSummary.qty.toLocaleString()} | 금액 합계:{" "} {totalSummary.amount.toLocaleString()}원 ) : ( "품목을 추가해주세요" )}
); } // --- 소스 데이터 테이블 컴포넌트들 --- function SourcePurchaseOrderTable({ data, onAdd, selectedKeys, }: { data: PurchaseOrderSource[]; onAdd: (po: PurchaseOrderSource) => void; selectedKeys: string[]; }) { if (data.length === 0) { return (
검색 버튼을 눌러 발주 데이터를 조회하세요
); } return ( 발주번호 공급처 품목 발주수량 입고수량 미입고 {data.map((po) => { const isSelected = selectedKeys.includes(`po-${po.id}`); return ( !isSelected && onAdd(po)} > {isSelected ? ( 추가됨 ) : ( )} {po.purchase_no} {po.supplier_name}
{po.item_name} {po.item_code} {po.spec ? ` | ${po.spec}` : ""}
{Number(po.order_qty).toLocaleString()} {Number(po.received_qty).toLocaleString()} {Number(po.remain_qty).toLocaleString()}
); })}
); } function SourceShipmentTable({ data, onAdd, selectedKeys, }: { data: ShipmentSource[]; onAdd: (sh: ShipmentSource) => void; selectedKeys: string[]; }) { if (data.length === 0) { return (
검색 버튼을 눌러 출하 데이터를 조회하세요
); } return ( 출하번호 출하일 거래처 품목 출하수량 {data.map((sh) => { const isSelected = selectedKeys.includes(`sh-${sh.detail_id}`); return ( !isSelected && onAdd(sh)} > {isSelected ? ( 추가됨 ) : ( )} {sh.instruction_no} {sh.instruction_date ? new Date(sh.instruction_date).toLocaleDateString("ko-KR") : "-"} {sh.partner_id}
{sh.item_name} {sh.item_code} {sh.spec ? ` | ${sh.spec}` : ""}
{Number(sh.ship_qty).toLocaleString()}
); })}
); } function SourceItemTable({ data, onAdd, selectedKeys, }: { data: ItemSource[]; onAdd: (item: ItemSource) => void; selectedKeys: string[]; }) { if (data.length === 0) { return (
검색 버튼을 눌러 품목 데이터를 조회하세요
); } return ( 품목 규격 재질 단위 기준가 {data.map((item) => { const isSelected = selectedKeys.includes(`item-${item.id}`); return ( !isSelected && onAdd(item)} > {isSelected ? ( 추가됨 ) : ( )}
{item.item_name} {item.item_number}
{item.spec || "-"} {item.material || "-"} {item.unit || "-"} {Number(item.standard_price).toLocaleString()}
); })}
); }