"use client"; import { useRouter } from "next/navigation"; import React, { useCallback, useEffect, useState } from "react"; import { apiClient } from "@/lib/api/client"; import { InspectionModal, type InspectionResult } from "./InspectionModal"; import type { PackageEntry } from "./NumberPadModal"; /* ------------------------------------------------------------------ */ /* Warehouse type */ /* ------------------------------------------------------------------ */ interface Warehouse { warehouse_code: string; warehouse_name: string; warehouse_type?: string; } /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ export interface CartItem { id: string; /** cart_items 테이블의 PK (UUID) — DB 삭제용 */ dbId?: string; /** purchase_detail or purchase_order_mng */ source_table: string; /** PK of the source row */ source_id: string; purchase_no: string; item_code: string; item_name: string; spec: string; material: string; order_qty: number; remain_qty: number; /** User-entered quantity */ inbound_qty: number; unit_price: number; supplier_code: string; supplier_name: string; order_date: string; inspection_required?: boolean; inspection_type?: "self" | "request" | null; packages?: PackageEntry[]; inspectionResult?: InspectionResult | null; } interface InboundCartProps { open: boolean; onClose: () => void; items: CartItem[]; onUpdateQty: (id: string, qty: number) => void; onRemove: (id: string) => void; onClear: () => void; supplierName?: string; onUpdateItems?: (items: CartItem[]) => void; } /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function InboundCart({ open, onClose, items, onUpdateQty, onRemove, onClear, supplierName, onUpdateItems, }: InboundCartProps) { const router = useRouter(); const [confirming, setConfirming] = useState(false); const [resultMsg, setResultMsg] = useState(null); const [selectedItems, setSelectedItems] = useState>(new Set()); const [inspectionModalOpen, setInspectionModalOpen] = useState(false); const [inspectionTarget, setInspectionTarget] = useState( null, ); /* Warehouse state */ const [warehouses, setWarehouses] = useState([]); const [selectedWarehouse, setSelectedWarehouse] = useState(""); /* Fetch warehouses on mount */ const fetchWarehouses = useCallback(async () => { try { const res = await apiClient.get("/receiving/warehouses"); const data: Warehouse[] = res.data?.data ?? []; setWarehouses(data); if (data.length > 0 && !selectedWarehouse) { setSelectedWarehouse(data[0].warehouse_code); } } catch { // Keep empty - user can still confirm without warehouse } }, [selectedWarehouse]); useEffect(() => { if (open) { fetchWarehouses(); } }, [open, fetchWarehouses]); const totalQty = items.reduce((s, i) => s + i.inbound_qty, 0); const totalAmount = items.reduce( (s, i) => s + i.inbound_qty * i.unit_price, 0, ); /* Toggle select */ const toggleSelect = (id: string) => { setSelectedItems((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const toggleSelectAll = () => { if (selectedItems.size === items.length) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(items.map((i) => i.id))); } }; /* Open inspection modal */ const openInspection = (item: CartItem) => { setInspectionTarget(item); setInspectionModalOpen(true); }; /* Handle inspection complete */ const handleInspectionComplete = (result: InspectionResult) => { if (!inspectionTarget || !onUpdateItems) return; const updated = items.map((item) => item.id === inspectionTarget.id ? { ...item, inspectionResult: result } : item, ); onUpdateItems(updated); setInspectionTarget(null); }; /* Confirm inbound — PC receivingController.create 와 동일한 body 구조 */ const handleConfirm = async () => { if (items.length === 0) return; if (!selectedWarehouse) { setResultMsg("오류: 입고 창고를 선택해주세요."); return; } setConfirming(true); setResultMsg(null); try { // 1. 입고번호 채번 (RCV-YYYY-XXXX) let inboundNumber: string | undefined; try { const numRes = await apiClient.get("/receiving/generate-number"); if (numRes.data?.success && numRes.data?.data) { inboundNumber = numRes.data.data; } } catch { // 채번 실패 시 백엔드가 처리 } // 2. POST /api/receiving — PC create 와 동일한 payload const payload = { inbound_number: inboundNumber, inbound_date: new Date().toISOString().slice(0, 10), warehouse_code: selectedWarehouse, inbound_type: "구매입고", items: items.map((item, idx) => ({ inbound_type: "구매입고", item_number: item.item_code, item_name: item.item_name, spec: item.spec || "", material: item.material || "", unit: "EA", inbound_qty: String(item.inbound_qty), unit_price: String(item.unit_price || 0), total_amount: String( (item.inbound_qty || 0) * (item.unit_price || 0), ), reference_number: item.purchase_no, supplier_code: item.supplier_code, supplier_name: item.supplier_name, inspection_status: item.inspectionResult?.completed ? "검사완료" : item.inspection_required ? "검사대기" : "합격", source_table: item.source_table, source_id: item.source_id || item.id, seq_no: idx + 1, })), }; const res = await apiClient.post("/receiving", payload); if (res.data?.success) { // 2-1. 검사 결과가 있는 항목 → inspection_result에 저장 const insertedDetails: any[] = res.data?.data?.details ?? res.data?.data?.items ?? []; const inboundHeaderNo = res.data?.data?.header?.inbound_number || inboundNumber || ""; const inspectionPromises = items .map((item, idx) => { if (!item.inspectionResult?.completed) return null; const matchedDetail = insertedDetails[idx] ?? {}; const referenceId = matchedDetail.id || matchedDetail.detail_id || `${inboundHeaderNo}-${idx + 1}`; const goodQty = item.inspectionResult.goodQty || 0; const badQty = item.inspectionResult.badQty || 0; const totalQty = goodQty + badQty; const overallJudgment = badQty === 0 ? "합격" : "불합격"; return apiClient .post("/pop/inspection-result", { inspectionNumber: item.inspectionResult.inspectionNumber, // 카트에서 받은 검사번호 재사용 referenceTable: "inbound_mng", referenceId, screenId: "pop_inbound_inspection", itemId: item.item_id || null, itemCode: item.item_code, itemName: item.item_name, inspectionType: "입고검사", overallJudgment, totalQty, goodQty, badQty, defectDescription: badQty > 0 ? `불량 ${badQty}건` : "", memo: item.inspectionResult.remark || "", supplierCode: item.supplier_code || null, supplierName: item.supplier_name || null, isCompleted: true, items: item.inspectionResult.items.map((insp: any) => ({ inspectionInfoId: insp.id || null, inspectionItemName: insp.inspection_item_name, inspectionStandard: insp.inspection_standard, passCriteria: insp.pass_criteria, isRequired: insp.is_required || "Y", measuredValue: insp.measured_value || "", judgment: insp.result || null, })), }) .catch((err) => { console.error( "[inspection_result 저장 실패]", item.item_code, err?.message, ); }); }) .filter(Boolean); if (inspectionPromises.length > 0) { await Promise.allSettled(inspectionPromises); } // 3. cart_items DB 정리 (백그라운드, 논블로킹) // cart_items.row_key 로 삭제 (row_key = source_id 로 저장됨) const rowKeys = items .map((item) => item.source_id || item.id) .filter(Boolean); if (rowKeys.length > 0) { apiClient .post("/pop/execute-action", { tasks: [{ type: "cart-save" }], cartChanges: { toDelete: rowKeys, }, }) .catch(() => { // cart cleanup 실패 시 무시 }); } const inboundNo = res.data?.data?.header?.inbound_number || inboundNumber || ""; setResultMsg(`${items.length}건 입고 등록 완료! (${inboundNo})`); setTimeout(() => { onClear(); onClose(); router.push("/pop/inbound"); }, 1500); } else { setResultMsg( `오류: ${res.data?.message || "입고 등록에 실패했습니다."}`, ); } } catch (err: unknown) { const msg = err instanceof Error ? err.message : "입고 등록에 실패했습니다."; setResultMsg(`오류: ${msg}`); } finally { setConfirming(false); } }; if (!open) return null; return (
{/* Overlay */}
{/* Panel */}
{/* Header */}

입고 장바구니

{supplierName && (

{supplierName}

)}
{/* Select all bar */} {items.length > 0 && (
전체 선택 ({selectedItems.size}/{items.length})
)} {/* Items */}
{items.length === 0 ? (

담은 품목이 없습니다

) : (
{items.map((item) => (
{/* Top row: checkbox + name + delete */}
{/* Checkbox */}

{item.item_name}

{item.item_code} | {item.purchase_no}

{/* Delete button */}
{/* Spec row */} {(item.spec || item.material) && (

{[item.spec, item.material].filter(Boolean).join(" | ")}

)} {/* Package info */} {item.packages && item.packages.length > 0 && (
포장완료 {"\uD83D\uDCE6"}{" "} {item.packages .map( (p) => `${p.count}${p.unit.label} x ${p.qtyPerUnit.toLocaleString()} = ${(p.count * p.qtyPerUnit).toLocaleString()}EA`, ) .join(", ")}
)} {/* Inspection row */} {(item.inspection_type === "self" || item.inspection_type === "request") && (
)} {/* Qty controls */}
미입고: {item.remain_qty.toLocaleString()}
{ const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 0) onUpdateQty(item.id, Math.min(v, item.remain_qty)); }} className="w-16 h-8 text-center text-sm font-semibold border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-100" style={{ fontVariantNumeric: "tabular-nums" }} />
))}
)}
{/* Footer summary + confirm */} {items.length > 0 && (
{/* Result message */} {resultMsg && (
{resultMsg}
)} {/* Warehouse selection */}
{/* Summary */}
총{" "} {items.length}
합계 수량:{" "} {totalQty.toLocaleString()} {totalAmount > 0 && ( ({totalAmount.toLocaleString()}원) )}
{/* Buttons */}
)}
{/* Inspection Modal */} {inspectionTarget && ( { setInspectionModalOpen(false); setInspectionTarget(null); }} onComplete={handleInspectionComplete} itemCode={inspectionTarget.item_code} itemName={inspectionTarget.item_name} totalQty={inspectionTarget.inbound_qty} initialResult={inspectionTarget.inspectionResult} /> )}
); }