"use client"; import React, { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { useRouter } from "next/navigation"; import { apiClient } from "@/lib/api/client"; import { CustomerModal, type Customer, matchChosung } from "./CustomerModal"; import { NumberPadModal, type PackageEntry } from "../inbound/NumberPadModal"; import { BarcodeScanModal } from "../common/BarcodeScanModal"; import type { CartItemWithId } from "../common/useCartSync"; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface ShipmentInstruction { id: string; detail_id: string; instruction_id: string; instruction_no: string; instruction_date: string; partner_id: string; customer_code: string; customer_name: string; item_code: string; item_name: string; spec: string; material: string; plan_qty: number; ship_qty: number; remain_qty: number; order_qty: number; source_type: string; /** Inspection type: "self" = self inspection required, "request" = inspection request optional, null = none */ inspection_type: "self" | "request" | null; /** Item image URL from item_info.image (may be null) */ image: string | null; } /* ------------------------------------------------------------------ */ /* Dummy data (fallback) */ /* ------------------------------------------------------------------ */ const DUMMY_INSTRUCTIONS: ShipmentInstruction[] = [ { id: "dm1", detail_id: "dm1", instruction_id: "ins1", instruction_no: "SI-2026-0401-001", instruction_date: "2026-03-30", partner_id: "C001", customer_code: "C001", customer_name: "(주)삼성전자", item_code: "STS304-2T", item_name: "STS304 2T 판재", spec: "2T x 1219 x 2438", material: "STS304", plan_qty: 500, ship_qty: 0, remain_qty: 500, order_qty: 500, source_type: "shipment_instruction_detail", inspection_type: null, image: null, }, { id: "dm2", detail_id: "dm2", instruction_id: "ins1", instruction_no: "SI-2026-0401-001", instruction_date: "2026-03-30", partner_id: "C001", customer_code: "C001", customer_name: "(주)삼성전자", item_code: "AL-A100", item_name: "알루미늄 프로파일 A100", spec: "A100 x 6000L", material: "AL6063", plan_qty: 200, ship_qty: 50, remain_qty: 150, order_qty: 200, source_type: "shipment_instruction_detail", inspection_type: null, image: null, }, { id: "dm3", detail_id: "dm3", instruction_id: "ins2", instruction_no: "SI-2026-0330-005", instruction_date: "2026-03-28", partner_id: "C002", customer_code: "C002", customer_name: "(주)LG전자", item_code: "BOLT-M10", item_name: "볼트 M10x30", spec: "M10x30 SUS", material: "SUS304", plan_qty: 1000, ship_qty: 0, remain_qty: 1000, order_qty: 1000, source_type: "shipment_instruction_detail", inspection_type: null, image: null, }, ]; /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ interface SalesOutboundProps { /** useCartSync hook instance (created in page.tsx and passed in) */ cart: import("../common/useCartSync").UseCartSyncReturn; } export function SalesOutbound({ cart }: SalesOutboundProps) { const router = useRouter(); /* State */ const [selectedCustomer, setSelectedCustomer] = useState(null); const [customerModalOpen, setCustomerModalOpen] = useState(false); const [instructions, setInstructions] = useState([]); const [loading, setLoading] = useState(false); const [keyword, setKeyword] = useState(""); /* NumberPad state */ const [numpadOpen, setNumpadOpen] = useState(false); const [numpadTarget, setNumpadTarget] = useState(null); /* Barcode scan modal state */ const [customerScanOpen, setCustomerScanOpen] = useState(false); const [itemScanOpen, setItemScanOpen] = useState(false); /* Inline customer search state */ const [allCustomers, setAllCustomers] = useState([]); /* Fetch all customers for inline search (customer_mng = 고객사) */ const fetchAllCustomers = useCallback(async () => { try { const res = await apiClient.get("/data/customer_mng", { params: { pageSize: 500 } }); const data = res.data?.data ?? res.data?.rows ?? []; const list: Customer[] = (Array.isArray(data) ? data : []).map((r: Record) => ({ id: String(r.id ?? ""), customer_name: String(r.customer_name ?? r.name ?? ""), customer_code: String(r.customer_code ?? r.code ?? ""), business_number: String(r.business_number ?? ""), phone: String(r.contact_phone ?? r.phone ?? ""), address: String(r.address ?? ""), })); setAllCustomers(list); } catch { setAllCustomers([]); } }, []); useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]); /* Fetch shipment instructions */ const fetchInstructions = useCallback(async (searchKeyword?: string) => { setLoading(true); try { const params: Record = { pageSize: "50" }; if (searchKeyword) params.keyword = searchKeyword; const res = await apiClient.get("/outbound/source/shipment-instructions", { params }); const data = res.data?.data; if (Array.isArray(data) && data.length > 0) { setInstructions(data.map((r: Record) => ({ id: String(r.detail_id ?? r.id ?? ""), detail_id: String(r.detail_id ?? ""), instruction_id: String(r.instruction_id ?? ""), instruction_no: String(r.instruction_no ?? ""), instruction_date: String(r.instruction_date ?? "").slice(0, 10), partner_id: String(r.partner_id ?? ""), customer_code: String(r.partner_id ?? ""), customer_name: "", // will be resolved from customer list or instruction item_code: String(r.item_code ?? ""), item_name: String(r.item_name ?? ""), spec: String(r.spec ?? ""), material: String(r.material ?? ""), plan_qty: Number(r.plan_qty ?? 0), ship_qty: Number(r.ship_qty ?? 0), remain_qty: Number(r.remain_qty ?? 0), order_qty: Number(r.order_qty ?? 0), source_type: String(r.source_type ?? "shipment_instruction_detail"), inspection_type: r.inspection_type === "self" ? "self" : r.inspection_type === "request" ? "request" : null, image: r.image ? String(r.image) : null, }))); } else { setInstructions(DUMMY_INSTRUCTIONS); } } catch { setInstructions(DUMMY_INSTRUCTIONS); } finally { setLoading(false); } }, []); /* Initial load */ useEffect(() => { fetchInstructions(); }, [fetchInstructions]); /* Filter instructions by selected customer */ const filteredInstructions = selectedCustomer ? instructions.filter((o) => o.customer_code === selectedCustomer.customer_code || o.partner_id === selectedCustomer.customer_code || o.partner_id === selectedCustomer.customer_name ) : instructions; /* Filter by keyword */ const displayInstructions = keyword ? filteredInstructions.filter((o) => o.item_name.toLowerCase().includes(keyword.toLowerCase()) || o.item_code.toLowerCase().includes(keyword.toLowerCase()) || o.instruction_no.toLowerCase().includes(keyword.toLowerCase()) ) : filteredInstructions; /* Open numpad for an instruction */ const openNumpad = (instr: ShipmentInstruction) => { setNumpadTarget(instr); setNumpadOpen(true); }; /* Add to cart with numpad result */ const handleNumpadConfirm = (qty: number, packages: PackageEntry[]) => { if (!numpadTarget) return; const instr = numpadTarget; if (cart.isItemInCart(instr.id)) return; const finalQty = Math.min(qty, instr.remain_qty); cart.addItem( { row: { id: instr.id, detail_id: instr.detail_id, instruction_id: instr.instruction_id, item_code: instr.item_code, item_name: instr.item_name, customer_code: instr.customer_code || instr.partner_id, customer_name: selectedCustomer?.customer_name || instr.customer_name, instruction_no: instr.instruction_no, instruction_date: instr.instruction_date, unit_price: 0, spec: instr.spec || "", material: instr.material || "", plan_qty: instr.plan_qty, remain_qty: instr.remain_qty, inspection_type: instr.inspection_type, source_table: "shipment_instruction_detail", source_type: "shipment_instruction_detail", source_id: instr.detail_id || instr.id, image: instr.image || null, }, quantity: finalQty, // eslint-disable-next-line @typescript-eslint/no-explicit-any packageEntries: packages.length > 0 ? packages as any : undefined, }, instr.id, ); setNumpadTarget(null); }; /* Remove from cart (cancel) */ const handleRemoveFromCart = (id: string) => { cart.removeItem(id); }; /* Search */ const handleSearch = () => { fetchInstructions(keyword || undefined); }; const isInCart = (id: string) => cart.isItemInCart(id); const getCartItem = (id: string): CartItemWithId | undefined => cart.getCartItem(id); return (
{/* ===== Header ===== */}

판매출고

출하지시 품목을 선택하여 출고하세요

{/* ===== Search area (2 columns on tablet+) ===== */}
{/* Customer search card */}
고객사 {selectedCustomer && ( {selectedCustomer.customer_name} )}
{/* QR/Barcode scan button */} {selectedCustomer && ( )}
{/* Item search card */}
출하지시 품목 {selectedCustomer ? displayInstructions.length : 0}
setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} placeholder="품목명, 품목코드, 출하지시번호 검색..." disabled={!selectedCustomer} className={`flex-1 px-3 py-2.5 border border-gray-200 rounded-lg text-sm outline-none transition-all ${ selectedCustomer ? "focus:border-green-400 focus:ring-2 focus:ring-green-100" : "bg-gray-50 text-gray-400 cursor-not-allowed" }`} /> {/* QR/Barcode scan button */}
{/* ===== Instruction items ===== */}
출하지시 품목 목록 {selectedCustomer ? `${displayInstructions.length}건` : "-"}
{!selectedCustomer ? (

고객사를 먼저 선택하세요

고객사를 선택하면 해당 고객사의 출하지시 품목이 표시됩니다

) : loading ? (
불러오는 중...
) : displayInstructions.length === 0 ? (

{selectedCustomer ? "해당 고객사의 미출고 출하지시가 없습니다" : "고객사를 선택하거나 품목을 검색하세요"}

) : (
{displayInstructions.map((instr) => { const inCart = isInCart(instr.id); const cartItem = getCartItem(instr.id); return (
{/* Green left bar for in-cart items */} {inCart && (
)} {/* === Header row: item code + item name === */}
{instr.item_code} {instr.item_name}
{/* === Body row: image + info + action === */}
{/* Product image */}
{instr.image ? ( {instr.item_name} ) : ( {"\uD83D\uDCE6"} )}
{/* Info columns */}
지시일 {instr.instruction_date}
지시번호 {instr.instruction_no}
지시수량 {instr.plan_qty.toLocaleString()}
미출고 {inCart ? (instr.remain_qty - (cartItem?.quantity ?? 0)).toLocaleString() : instr.remain_qty.toLocaleString() }
{/* Action column: qty display + add/cancel button */}
{/* Qty display */} {/* Add / Cancel button */} {inCart ? ( ) : ( )}
{/* === Package info (shown when in cart with packages) === */} {(() => { const pkgs = (inCart && cartItem?.packageEntries) ? cartItem.packageEntries as unknown as PackageEntry[] : []; return pkgs.length > 0 && (
포장완료 {pkgs.reduce((s, p) => s + p.count * p.qtyPerUnit, 0).toLocaleString()} EA
{pkgs.map((pkg, idx) => (
{pkg.unit.icon} {pkg.count}{pkg.unit.label} x {pkg.qtyPerUnit.toLocaleString()}EA = {(pkg.count * pkg.qtyPerUnit).toLocaleString()}EA
))}
); })()}
); })}
)}
{/* ===== Modals ===== */} setCustomerModalOpen(false)} onSelect={(c) => setSelectedCustomer(c)} /> { setNumpadOpen(false); setNumpadTarget(null); }} onConfirm={handleNumpadConfirm} maxQty={numpadTarget?.remain_qty ?? 0} itemName={numpadTarget?.item_name ?? ""} /> {/* Barcode scan modal for customer */} { setCustomerScanOpen(false); const match = allCustomers.find( (c) => c.customer_code === barcode || c.customer_name.includes(barcode) ); if (match) { setSelectedCustomer(match); } }} /> {/* Barcode scan modal for item */} { setItemScanOpen(false); setKeyword(barcode); fetchInstructions(barcode); }} />
); }