Merge branch 'mhkim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -180,7 +180,7 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
item.outbound_status || "대기",
|
||||
manager_id || item.manager_id || null,
|
||||
memo || item.memo || null,
|
||||
item.source_type || null,
|
||||
item.source_type || item.source_table || null,
|
||||
item.sales_order_id || null,
|
||||
item.shipment_plan_id || null,
|
||||
item.item_info_id || null,
|
||||
@@ -275,11 +275,12 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
);
|
||||
}
|
||||
|
||||
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
|
||||
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영 + master status 자동 전환
|
||||
const itemSourceTable = item.source_type || item.source_table;
|
||||
if (
|
||||
item.outbound_type === "판매출고" &&
|
||||
item.source_id &&
|
||||
item.source_type === "shipment_instruction_detail"
|
||||
itemSourceTable === "shipment_instruction_detail"
|
||||
) {
|
||||
const outQtyNum = Number(item.outbound_qty) || 0;
|
||||
await client.query(
|
||||
@@ -292,9 +293,12 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
|
||||
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
|
||||
const sidRes = await client.query(
|
||||
`SELECT detail_id FROM shipment_instruction_detail WHERE id = $1 AND company_code = $2`,
|
||||
`SELECT instruction_id, detail_id
|
||||
FROM shipment_instruction_detail
|
||||
WHERE id = $1 AND company_code = $2`,
|
||||
[item.source_id, companyCode],
|
||||
);
|
||||
const instructionId = sidRes.rows[0]?.instruction_id;
|
||||
const detailId = sidRes.rows[0]?.detail_id;
|
||||
if (detailId) {
|
||||
await client.query(
|
||||
@@ -306,6 +310,27 @@ export async function create(req: AuthenticatedRequest, res: Response) {
|
||||
[outQtyNum, detailId, companyCode],
|
||||
);
|
||||
}
|
||||
|
||||
// shipment_instruction master status 자동 전환 (입고의 purchase_detail → purchase_order_mng 패턴)
|
||||
// 모든 detail 의 잔량이 0 이면 COMPLETED, 아니면 IN_PROGRESS
|
||||
if (instructionId) {
|
||||
const unshippedRes = await client.query(
|
||||
`SELECT COUNT(*)::int AS unshipped
|
||||
FROM shipment_instruction_detail
|
||||
WHERE instruction_id = $1 AND company_code = $2
|
||||
AND COALESCE(plan_qty, 0) > COALESCE(ship_qty, 0)`,
|
||||
[instructionId, companyCode],
|
||||
);
|
||||
const unshipped = unshippedRes.rows[0]?.unshipped ?? 0;
|
||||
const newStatus = unshipped === 0 ? "COMPLETED" : "IN_PROGRESS";
|
||||
await client.query(
|
||||
`UPDATE shipment_instruction
|
||||
SET status = $1,
|
||||
updated_date = NOW()
|
||||
WHERE id = $2 AND company_code = $3`,
|
||||
[newStatus, instructionId, companyCode],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,12 +594,18 @@ export async function getShipmentInstructions(
|
||||
) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { keyword } = req.query;
|
||||
const { keyword, customer } = req.query;
|
||||
|
||||
const conditions: string[] = ["si.company_code = $1"];
|
||||
const params: any[] = [companyCode];
|
||||
let paramIdx = 2;
|
||||
|
||||
if (customer) {
|
||||
conditions.push(`si.partner_id = $${paramIdx}`);
|
||||
params.push(customer);
|
||||
paramIdx++;
|
||||
}
|
||||
|
||||
if (keyword) {
|
||||
conditions.push(
|
||||
`(si.instruction_no ILIKE $${paramIdx} OR sid.item_name ILIKE $${paramIdx} OR sid.item_code ILIKE $${paramIdx})`,
|
||||
@@ -591,6 +622,7 @@ export async function getShipmentInstructions(
|
||||
si.instruction_no,
|
||||
si.instruction_date,
|
||||
si.partner_id,
|
||||
COALESCE(c.customer_name, si.partner_id, '') AS customer_name,
|
||||
si.status AS instruction_status,
|
||||
sid.item_code,
|
||||
sid.item_name,
|
||||
@@ -605,6 +637,9 @@ export async function getShipmentInstructions(
|
||||
JOIN shipment_instruction_detail sid
|
||||
ON si.id = sid.instruction_id
|
||||
AND si.company_code = sid.company_code
|
||||
LEFT JOIN customer_mng c
|
||||
ON si.partner_id = c.customer_code
|
||||
AND si.company_code = c.company_code
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
AND COALESCE(sid.plan_qty, 0) > COALESCE(sid.ship_qty, 0)
|
||||
ORDER BY si.instruction_date DESC, si.instruction_no`,
|
||||
|
||||
@@ -17,30 +17,32 @@ router.get("/info", async (req: Request, res: Response) => {
|
||||
return res.status(401).json({ success: false, message: "인증 정보 없음" });
|
||||
}
|
||||
|
||||
const conditions: string[] = ["company_code = $1", "is_active = 'Y'"];
|
||||
const conditions: string[] = ["iii.company_code = $1", "iii.is_active = '사용'"];
|
||||
const params: unknown[] = [companyCode];
|
||||
let idx = 2;
|
||||
|
||||
if (itemCode) {
|
||||
conditions.push(`item_code = $${idx++}`);
|
||||
conditions.push(`iii.item_code = $${idx++}`);
|
||||
params.push(itemCode);
|
||||
}
|
||||
if (itemId) {
|
||||
conditions.push(`item_id = $${idx++}`);
|
||||
conditions.push(`iii.item_id = $${idx++}`);
|
||||
params.push(itemId);
|
||||
}
|
||||
if (inspectionType) {
|
||||
conditions.push(`inspection_type = $${idx++}`);
|
||||
conditions.push(`iii.inspection_type = $${idx++}`);
|
||||
params.push(inspectionType);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT id, item_id, item_code, item_name,
|
||||
inspection_type, inspection_item_name, inspection_standard,
|
||||
inspection_method, pass_criteria, is_required, sort_order, memo
|
||||
FROM item_inspection_info
|
||||
SELECT iii.id, iii.item_id, iii.item_code, iii.item_name,
|
||||
iii.inspection_type, iii.inspection_item_name, iii.inspection_standard,
|
||||
iii.inspection_method, iii.pass_criteria, iii.is_required, iii.sort_order, iii.memo,
|
||||
ist.judgment_criteria
|
||||
FROM item_inspection_info iii
|
||||
LEFT JOIN inspection_standard ist ON ist.id = iii.inspection_standard_id
|
||||
WHERE ${conditions.join(" AND ")}
|
||||
ORDER BY sort_order, inspection_item_name
|
||||
ORDER BY iii.sort_order, iii.inspection_item_name
|
||||
`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
|
||||
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
|
||||
const CUSTOMER_SOURCE: PartnerSourceConfig = {
|
||||
tableName: "customer_mng",
|
||||
fields: {
|
||||
code: "customer_code",
|
||||
name: "customer_name",
|
||||
businessNumber: "business_number",
|
||||
phone: "contact_phone",
|
||||
address: "address",
|
||||
},
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -71,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -83,10 +100,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const customerDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* Fetch all customers for inline search
|
||||
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
|
||||
*/
|
||||
const fetchAllCustomers = useCallback(async () => {
|
||||
setAllCustomers([]);
|
||||
try {
|
||||
const res = await apiClient.post(
|
||||
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
|
||||
{
|
||||
page: 1,
|
||||
size: 500,
|
||||
autoFilter: true,
|
||||
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
|
||||
},
|
||||
);
|
||||
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
|
||||
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => ({
|
||||
id: String(r.id ?? ""),
|
||||
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
|
||||
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
|
||||
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
|
||||
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
|
||||
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
|
||||
}),
|
||||
);
|
||||
setAllCustomers(list);
|
||||
} catch {
|
||||
setAllCustomers([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
|
||||
@@ -135,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -219,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
@@ -543,6 +628,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
open={customerModalOpen}
|
||||
onClose={() => setCustomerModalOpen(false)}
|
||||
onSelect={(s) => selectCustomer(s)}
|
||||
source={CUSTOMER_SOURCE}
|
||||
/>
|
||||
|
||||
<SimpleKeypadModal
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
|
||||
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
|
||||
const CUSTOMER_SOURCE: PartnerSourceConfig = {
|
||||
tableName: "customer_mng",
|
||||
fields: {
|
||||
code: "customer_code",
|
||||
name: "customer_name",
|
||||
businessNumber: "business_number",
|
||||
phone: "contact_phone",
|
||||
address: "address",
|
||||
},
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -71,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -83,10 +100,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const customerDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* Fetch all customers for inline search
|
||||
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
|
||||
*/
|
||||
const fetchAllCustomers = useCallback(async () => {
|
||||
setAllCustomers([]);
|
||||
try {
|
||||
const res = await apiClient.post(
|
||||
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
|
||||
{
|
||||
page: 1,
|
||||
size: 500,
|
||||
autoFilter: true,
|
||||
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
|
||||
},
|
||||
);
|
||||
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
|
||||
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => ({
|
||||
id: String(r.id ?? ""),
|
||||
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
|
||||
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
|
||||
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
|
||||
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
|
||||
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
|
||||
}),
|
||||
);
|
||||
setAllCustomers(list);
|
||||
} catch {
|
||||
setAllCustomers([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
|
||||
@@ -135,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -219,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
@@ -543,6 +628,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
open={customerModalOpen}
|
||||
onClose={() => setCustomerModalOpen(false)}
|
||||
onSelect={(s) => selectCustomer(s)}
|
||||
source={CUSTOMER_SOURCE}
|
||||
/>
|
||||
|
||||
<SimpleKeypadModal
|
||||
|
||||
@@ -51,6 +51,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "/pop/production",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "quality",
|
||||
title: "품질",
|
||||
@@ -75,6 +76,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "#",
|
||||
},
|
||||
*/
|
||||
{
|
||||
id: "inventory",
|
||||
title: "재고",
|
||||
@@ -87,6 +89,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "/pop/inventory",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "safety",
|
||||
title: "안전관리",
|
||||
@@ -99,6 +102,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "#",
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
function LocalMenuIcons() {
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePopCompanyPath } from "@/hooks/usePopCompanyPath";
|
||||
import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung } from "../inbound/SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
/* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 매핑 */
|
||||
const CUSTOMER_SOURCE: PartnerSourceConfig = {
|
||||
tableName: "customer_mng",
|
||||
fields: {
|
||||
code: "customer_code",
|
||||
name: "customer_name",
|
||||
businessNumber: "business_number",
|
||||
phone: "contact_phone",
|
||||
address: "address",
|
||||
},
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -71,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -83,10 +100,34 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const customerDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* Fetch all customers for inline search
|
||||
* TODO: API 연결 — 판매출고용 거래처 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 거래처관리 화면과 동일한 customer_mng 테이블 조회
|
||||
*/
|
||||
const fetchAllCustomers = useCallback(async () => {
|
||||
setAllCustomers([]);
|
||||
try {
|
||||
const res = await apiClient.post(
|
||||
`/table-management/tables/${CUSTOMER_SOURCE.tableName}/data`,
|
||||
{
|
||||
page: 1,
|
||||
size: 500,
|
||||
autoFilter: true,
|
||||
sort: { columnName: CUSTOMER_SOURCE.fields.code, order: "desc" },
|
||||
},
|
||||
);
|
||||
const rows = res.data?.data?.data ?? res.data?.data?.rows ?? [];
|
||||
const list: Supplier[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => ({
|
||||
id: String(r.id ?? ""),
|
||||
customer_name: String(r[CUSTOMER_SOURCE.fields.name] ?? ""),
|
||||
customer_code: String(r[CUSTOMER_SOURCE.fields.code] ?? ""),
|
||||
business_number: String(r[CUSTOMER_SOURCE.fields.businessNumber!] ?? ""),
|
||||
phone: String(r[CUSTOMER_SOURCE.fields.phone!] ?? ""),
|
||||
address: String(r[CUSTOMER_SOURCE.fields.address!] ?? ""),
|
||||
}),
|
||||
);
|
||||
setAllCustomers(list);
|
||||
} catch {
|
||||
setAllCustomers([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { fetchAllCustomers(); }, [fetchAllCustomers]);
|
||||
@@ -135,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -219,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
@@ -543,6 +628,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
open={customerModalOpen}
|
||||
onClose={() => setCustomerModalOpen(false)}
|
||||
onSelect={(s) => selectCustomer(s)}
|
||||
source={CUSTOMER_SOURCE}
|
||||
/>
|
||||
|
||||
<SimpleKeypadModal
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -84,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -172,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -256,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
|
||||
@@ -51,6 +51,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "/pop/production",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "quality",
|
||||
title: "품질",
|
||||
@@ -75,6 +76,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "#",
|
||||
},
|
||||
*/
|
||||
{
|
||||
id: "inventory",
|
||||
title: "재고",
|
||||
@@ -87,6 +89,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "/pop/inventory",
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: "safety",
|
||||
title: "안전관리",
|
||||
@@ -99,6 +102,7 @@ const MENU_ITEMS: MenuIconItem[] = [
|
||||
),
|
||||
href: "#",
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
function LocalMenuIcons() {
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -84,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -172,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -256,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -84,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -172,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -256,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface InspectionItem {
|
||||
inspection_method: string;
|
||||
pass_criteria: string;
|
||||
is_required: string;
|
||||
/** "CAT_JC_01" 수치(범위) | "CAT_JC_02" 텍스트입력 | "CAT_JC_03" O/X | "CAT_JC_04" 선택형 */
|
||||
judgment_criteria?: string;
|
||||
/** User-entered measured value */
|
||||
measured_value: string;
|
||||
/** "pass" | "fail" | null */
|
||||
@@ -143,6 +145,7 @@ export function InspectionModal({
|
||||
inspection_method: String(r.inspection_method ?? ""),
|
||||
pass_criteria: String(r.pass_criteria ?? ""),
|
||||
is_required: String(r.is_required ?? "Y"),
|
||||
judgment_criteria: String(r.judgment_criteria ?? ""),
|
||||
measured_value: "",
|
||||
result: null,
|
||||
})),
|
||||
@@ -397,23 +400,25 @@ export function InspectionModal({
|
||||
|
||||
{/* Input + result buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
{item.judgment_criteria !== "CAT_JC_03" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
openNumpad(
|
||||
`${item.inspection_item_name} - 측정값`,
|
||||
item.measured_value,
|
||||
(v) => updateItem(item.id, "measured_value", v),
|
||||
)
|
||||
}
|
||||
className={`flex-1 h-9 px-2.5 text-[13px] border rounded-md text-left transition-all ${
|
||||
item.measured_value
|
||||
? "bg-blue-50 border-blue-200 text-blue-700 font-semibold"
|
||||
: "bg-white border-gray-200 text-gray-400 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
{item.measured_value || "측정값 입력"}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => updateItem(item.id, "result", "pass")}
|
||||
|
||||
@@ -84,6 +84,10 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
const [numpadOpen, setNumpadOpen] = useState(false);
|
||||
const [numpadTarget, setNumpadTarget] = useState<OutboundOrder | null>(null);
|
||||
|
||||
/* Ref to always call the latest saveToDb (avoids stale closure in setTimeout) */
|
||||
const saveToDbRef = useRef(cart.saveToDb);
|
||||
useEffect(() => { saveToDbRef.current = cart.saveToDb; });
|
||||
|
||||
/* Barcode scan modal state */
|
||||
const [customerScanOpen, setCustomerScanOpen] = useState(false);
|
||||
const [itemScanOpen, setItemScanOpen] = useState(false);
|
||||
@@ -172,19 +176,63 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
}, []);
|
||||
|
||||
/* Fetch outbound orders
|
||||
* TODO: API 연결 — 판매출고 대상 품목 조회 엔드포인트 확정 후 연동
|
||||
* 영업관리 > 출하지시관리 (shipment_instruction + shipment_instruction_detail) 의 잔량 있는 항목 조회
|
||||
* 거래처 선택 후에만 호출. 잔량(plan_qty - ship_qty) > 0 만 백엔드에서 자동 필터.
|
||||
*/
|
||||
const fetchOrders = useCallback(async (_searchKeyword?: string) => {
|
||||
const fetchOrders = useCallback(async (searchKeyword?: string) => {
|
||||
if (!selectedCustomer?.customer_code) {
|
||||
setOrders([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
try {
|
||||
const params: Record<string, string> = {
|
||||
customer: selectedCustomer.customer_code,
|
||||
};
|
||||
if (searchKeyword?.trim()) params.keyword = searchKeyword.trim();
|
||||
const res = await apiClient.get("/outbound/source/shipment-instructions", { params });
|
||||
const rows = res.data?.data ?? [];
|
||||
const mapped: OutboundOrder[] = (Array.isArray(rows) ? rows : []).map(
|
||||
(r: Record<string, unknown>) => {
|
||||
const planQty = Number(r.plan_qty ?? 0);
|
||||
const shipQty = Number(r.ship_qty ?? 0);
|
||||
const orderQty = Number(r.order_qty ?? 0);
|
||||
const remainQty = Number(r.remain_qty ?? Math.max(planQty - shipQty, 0));
|
||||
return {
|
||||
id: String(r.detail_id ?? ""),
|
||||
reference_no: String(r.instruction_no ?? ""),
|
||||
order_date: String(r.instruction_date ?? ""),
|
||||
customer_code: String(r.partner_id ?? ""),
|
||||
customer_name: String(r.customer_name ?? selectedCustomer.customer_name ?? ""),
|
||||
item_code: String(r.item_code ?? ""),
|
||||
item_name: String(r.item_name ?? ""),
|
||||
spec: String(r.spec ?? ""),
|
||||
material: String(r.material ?? ""),
|
||||
order_qty: orderQty || planQty,
|
||||
shipped_qty: shipQty,
|
||||
remain_qty: remainQty,
|
||||
unit_price: 0,
|
||||
status: String(r.instruction_status ?? ""),
|
||||
due_date: String(r.instruction_date ?? ""),
|
||||
source_table: "shipment_instruction_detail",
|
||||
inspection_type: null,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
setOrders(mapped);
|
||||
} catch (err) {
|
||||
setOrders([]);
|
||||
setFetchError(
|
||||
err instanceof Error ? err.message : "출하지시 조회에 실패했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedCustomer]);
|
||||
|
||||
/* Initial load */
|
||||
/* 거래처 선택 변경 시 재조회 */
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
@@ -256,13 +304,13 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
sourceTable,
|
||||
);
|
||||
setNumpadTarget(null);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Remove from cart (cancel) */
|
||||
const handleRemoveFromCart = (id: string) => {
|
||||
cart.removeItem(id);
|
||||
setTimeout(() => cart.saveToDb().catch(() => {}), 300);
|
||||
setTimeout(() => saveToDbRef.current().catch(() => {}), 300);
|
||||
};
|
||||
|
||||
/* Search */
|
||||
|
||||
Reference in New Issue
Block a user