feat: 생산완료→재고 자동입고 + 목표창고 API + 작업상세 창고선택 UI
- popProductionController: 작업지시 전체완료 시 마지막 공정의 목표창고로 inventory_stock UPSERT - popProductionRoutes: 창고 목록/위치 조회, 마지막 공정 확인, 목표창고 업데이트 4개 API 추가 - PopWorkDetailComponent: 마지막 공정일 때 창고/위치 선택 UI 표시 (토글 지원)
This commit is contained in:
parent
768219046b
commit
ee8c1eb0df
|
|
@ -985,7 +985,7 @@ const checkAndCompleteWorkInstruction = async (
|
|||
|
||||
const completedQty = totalGoodResult.rows[0].total_good;
|
||||
|
||||
await pool.query(
|
||||
const updateResult = await pool.query(
|
||||
`UPDATE work_instruction
|
||||
SET status = 'completed',
|
||||
progress_status = 'completed',
|
||||
|
|
@ -993,13 +993,69 @@ const checkAndCompleteWorkInstruction = async (
|
|||
writer = $4,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
AND status != 'completed'`,
|
||||
AND status != 'completed'
|
||||
RETURNING id, item_id`,
|
||||
[woId, companyCode, String(completedQty), userId]
|
||||
);
|
||||
|
||||
logger.info("[pop/production] 작업지시 전체 완료", {
|
||||
woId, completedQty, companyCode,
|
||||
});
|
||||
|
||||
// 생산완료→재고 입고: 마지막 공정의 target_warehouse_id가 설정된 경우 inventory_stock UPSERT
|
||||
if (updateResult.rowCount > 0 && completedQty > 0) {
|
||||
try {
|
||||
const itemId = updateResult.rows[0].item_id;
|
||||
|
||||
// item_info에서 item_number 조회
|
||||
const itemResult = await pool.query(
|
||||
`SELECT item_number FROM item_info WHERE id = $1 AND company_code = $2`,
|
||||
[itemId, companyCode]
|
||||
);
|
||||
if (itemResult.rowCount === 0) {
|
||||
logger.warn("[pop/production] 재고입고 건너뜀: item_info 없음", { itemId, companyCode });
|
||||
return;
|
||||
}
|
||||
const itemCode = itemResult.rows[0].item_number;
|
||||
|
||||
// 마지막 공정의 창고 설정 조회 (마스터 행에서)
|
||||
const warehouseResult = await pool.query(
|
||||
`SELECT target_warehouse_id, target_location_code
|
||||
FROM work_order_process
|
||||
WHERE wo_id = $1 AND seq_no = $2 AND company_code = $3
|
||||
AND parent_process_id IS NULL
|
||||
LIMIT 1`,
|
||||
[woId, maxSeq, companyCode]
|
||||
);
|
||||
|
||||
if (warehouseResult.rowCount === 0 || !warehouseResult.rows[0].target_warehouse_id) {
|
||||
logger.info("[pop/production] 재고입고 건너뜀: 목표창고 미설정", { woId });
|
||||
return;
|
||||
}
|
||||
|
||||
const warehouseCode = warehouseResult.rows[0].target_warehouse_id;
|
||||
const locationCode = warehouseResult.rows[0].target_location_code || warehouseCode;
|
||||
|
||||
// inventory_stock UPSERT
|
||||
await pool.query(
|
||||
`INSERT INTO inventory_stock (id, company_code, item_code, warehouse_code, location_code, current_qty, last_in_date, created_date, updated_date, writer)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, NOW(), NOW(), NOW(), $6)
|
||||
ON CONFLICT (company_code, item_code, warehouse_code, location_code)
|
||||
DO UPDATE SET current_qty = (COALESCE(inventory_stock.current_qty::numeric, 0) + $5::numeric)::text,
|
||||
last_in_date = NOW(),
|
||||
updated_date = NOW(),
|
||||
writer = $6`,
|
||||
[companyCode, itemCode, warehouseCode, locationCode, String(completedQty), userId]
|
||||
);
|
||||
|
||||
logger.info("[pop/production] 생산완료→재고 입고 완료", {
|
||||
woId, itemCode, warehouseCode, locationCode, qty: completedQty, companyCode,
|
||||
});
|
||||
} catch (inventoryError: any) {
|
||||
// 재고 입고 실패해도 공정 완료는 유지 (재고는 보조 기능)
|
||||
logger.error("[pop/production] 재고입고 오류 (공정 완료는 유지):", inventoryError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1607,3 +1663,199 @@ export const cancelAccept = async (
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 창고 목록 조회 (POP 생산용)
|
||||
*/
|
||||
export const getWarehouses = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const result = await pool.query(
|
||||
`SELECT id, warehouse_code, warehouse_name, warehouse_type
|
||||
FROM warehouse_info
|
||||
WHERE company_code = $1 AND COALESCE(status, '') != '삭제'
|
||||
ORDER BY warehouse_name`,
|
||||
[companyCode]
|
||||
);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] 창고 목록 조회 실패:", error);
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 창고의 위치(로케이션) 목록 조회
|
||||
* warehouseId는 warehouse_info.id → warehouse_code를 조회해서 warehouse_location과 매칭
|
||||
*/
|
||||
export const getWarehouseLocations = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { warehouseId } = req.params;
|
||||
if (!warehouseId) {
|
||||
return res.status(400).json({ success: false, message: "warehouseId는 필수입니다." });
|
||||
}
|
||||
|
||||
// warehouse_info.id → warehouse_code 변환
|
||||
const whInfo = await pool.query(
|
||||
`SELECT warehouse_code FROM warehouse_info WHERE id = $1 AND company_code = $2`,
|
||||
[warehouseId, companyCode]
|
||||
);
|
||||
if (whInfo.rowCount === 0) {
|
||||
return res.json({ success: true, data: [] });
|
||||
}
|
||||
const warehouseCode = whInfo.rows[0].warehouse_code;
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT id, location_code, location_name
|
||||
FROM warehouse_location
|
||||
WHERE warehouse_code = $1 AND company_code = $2
|
||||
ORDER BY location_name`,
|
||||
[warehouseCode, companyCode]
|
||||
);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] 창고 위치 조회 실패:", error);
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 마지막 공정 여부 확인
|
||||
* 같은 wo_id에서 현재 seq_no보다 큰 공정(마스터 행)이 없으면 마지막
|
||||
*/
|
||||
export const isLastProcess = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { processId } = req.params;
|
||||
if (!processId) {
|
||||
return res.json({ success: true, data: { isLast: false } });
|
||||
}
|
||||
|
||||
// 현재 공정의 wo_id와 seq_no 조회 (분할 행이면 parent의 seq_no 기준)
|
||||
const process = await pool.query(
|
||||
`SELECT wo_id, seq_no, parent_process_id
|
||||
FROM work_order_process
|
||||
WHERE id = $1 AND company_code = $2`,
|
||||
[processId, companyCode]
|
||||
);
|
||||
if (process.rowCount === 0) {
|
||||
return res.json({ success: true, data: { isLast: false } });
|
||||
}
|
||||
|
||||
const { wo_id, seq_no, parent_process_id } = process.rows[0];
|
||||
|
||||
// 분할 행이면 마스터의 seq_no 기준으로 판단
|
||||
let effectiveSeqNo = seq_no;
|
||||
if (parent_process_id) {
|
||||
const master = await pool.query(
|
||||
`SELECT seq_no FROM work_order_process WHERE id = $1 AND company_code = $2`,
|
||||
[parent_process_id, companyCode]
|
||||
);
|
||||
if (master.rowCount > 0) {
|
||||
effectiveSeqNo = master.rows[0].seq_no;
|
||||
}
|
||||
}
|
||||
|
||||
const next = await pool.query(
|
||||
`SELECT id FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2
|
||||
AND CAST(seq_no AS int) > CAST($3 AS int)
|
||||
AND parent_process_id IS NULL
|
||||
LIMIT 1`,
|
||||
[wo_id, companyCode, effectiveSeqNo]
|
||||
);
|
||||
|
||||
// 현재 공정의 기존 창고 설정도 반환 (기본값 세팅용)
|
||||
const warehouseInfo = await pool.query(
|
||||
`SELECT target_warehouse_id, target_location_code
|
||||
FROM work_order_process
|
||||
WHERE id = $1 AND company_code = $2`,
|
||||
[processId, companyCode]
|
||||
);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
isLast: next.rowCount === 0,
|
||||
woId: wo_id,
|
||||
seqNo: effectiveSeqNo,
|
||||
targetWarehouseId: warehouseInfo.rows[0]?.target_warehouse_id || null,
|
||||
targetLocationCode: warehouseInfo.rows[0]?.target_location_code || null,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] 마지막 공정 확인 오류:", error);
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 공정의 목표 창고/위치 업데이트
|
||||
* 마지막 공정 완료 전 또는 완료 후 창고를 지정한다.
|
||||
* 마스터 행에 저장하여 checkAndCompleteWorkInstruction이 참조할 수 있도록 한다.
|
||||
*/
|
||||
export const updateTargetWarehouse = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { work_order_process_id, target_warehouse_id, target_location_code } = req.body;
|
||||
|
||||
if (!work_order_process_id || !target_warehouse_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "work_order_process_id와 target_warehouse_id는 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 분할 행이면 마스터 행도 함께 업데이트
|
||||
const procInfo = await pool.query(
|
||||
`SELECT parent_process_id FROM work_order_process WHERE id = $1 AND company_code = $2`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
|
||||
const idsToUpdate = [work_order_process_id];
|
||||
if (procInfo.rowCount > 0 && procInfo.rows[0].parent_process_id) {
|
||||
idsToUpdate.push(procInfo.rows[0].parent_process_id);
|
||||
}
|
||||
|
||||
for (const id of idsToUpdate) {
|
||||
await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET target_warehouse_id = $3,
|
||||
target_location_code = $4,
|
||||
writer = $5,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2`,
|
||||
[id, companyCode, target_warehouse_id, target_location_code || null, userId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("[pop/production] 목표 창고 업데이트", {
|
||||
companyCode, userId, work_order_process_id,
|
||||
target_warehouse_id, target_location_code,
|
||||
updatedIds: idsToUpdate,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: { target_warehouse_id, target_location_code } });
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] 목표 창고 업데이트 오류:", error);
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import {
|
|||
getAvailableQty,
|
||||
acceptProcess,
|
||||
cancelAccept,
|
||||
getWarehouses,
|
||||
getWarehouseLocations,
|
||||
isLastProcess,
|
||||
updateTargetWarehouse,
|
||||
} from "../controllers/popProductionController";
|
||||
|
||||
const router = Router();
|
||||
|
|
@ -27,5 +31,9 @@ router.get("/result-history", getResultHistory);
|
|||
router.get("/available-qty", getAvailableQty);
|
||||
router.post("/accept-process", acceptProcess);
|
||||
router.post("/cancel-accept", cancelAccept);
|
||||
router.get("/warehouses", getWarehouses);
|
||||
router.get("/warehouse-locations/:warehouseId", getWarehouseLocations);
|
||||
router.get("/is-last-process/:processId", isLastProcess);
|
||||
router.post("/update-target-warehouse", updateTargetWarehouse);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ import { dataApi } from "@/lib/api/data";
|
|||
import { apiClient } from "@/lib/api/client";
|
||||
import { usePopEvent } from "@/hooks/pop/usePopEvent";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { PopWorkDetailConfig, ResultSectionConfig, ResultSectionType, PlcDataConfig } from "../types";
|
||||
import type { TimelineProcessStep } from "../types";
|
||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||
|
|
@ -344,6 +350,16 @@ export function PopWorkDetailComponent({
|
|||
// 불량 유형 목록 (부모에서 1회 로드, ResultPanel에 전달)
|
||||
const [cachedDefectTypes, setCachedDefectTypes] = useState<DefectTypeOption[]>([]);
|
||||
|
||||
// 창고 선택 모달 상태
|
||||
const [showWarehouseModal, setShowWarehouseModal] = useState(false);
|
||||
const [warehouses, setWarehouses] = useState<{ id: string; warehouse_code: string; warehouse_name: string; warehouse_type: string }[]>([]);
|
||||
const [locations, setLocations] = useState<{ id: string; location_code: string; location_name: string }[]>([]);
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState("");
|
||||
const [selectedLocation, setSelectedLocation] = useState("");
|
||||
const [useDefaultWarehouse, setUseDefaultWarehouse] = useState(false);
|
||||
const [warehouseSaving, setWarehouseSaving] = useState(false);
|
||||
const [pendingCompletionData, setPendingCompletionData] = useState<Partial<ProcessTimerData> | null>(null);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ========================================
|
||||
|
|
@ -397,6 +413,128 @@ export function PopWorkDetailComponent({
|
|||
loadDefectTypes();
|
||||
}, []);
|
||||
|
||||
// ========================================
|
||||
// 창고 선택 (마지막 공정 완료 시)
|
||||
// ========================================
|
||||
|
||||
const loadWarehouses = useCallback(async () => {
|
||||
try {
|
||||
const res = await apiClient.get("/pop/production/warehouses");
|
||||
if (res.data?.success) {
|
||||
setWarehouses(res.data.data || []);
|
||||
}
|
||||
} catch { /* 실패 시 빈 배열 유지 */ }
|
||||
}, []);
|
||||
|
||||
const loadLocations = useCallback(async (warehouseId: string) => {
|
||||
if (!warehouseId) { setLocations([]); return; }
|
||||
try {
|
||||
const res = await apiClient.get(`/pop/production/warehouse-locations/${warehouseId}`);
|
||||
if (res.data?.success) {
|
||||
setLocations(res.data.data || []);
|
||||
}
|
||||
} catch { setLocations([]); }
|
||||
}, []);
|
||||
|
||||
// 창고 선택 변경 시 위치 목록 갱신
|
||||
useEffect(() => {
|
||||
if (selectedWarehouse) {
|
||||
loadLocations(selectedWarehouse);
|
||||
setSelectedLocation("");
|
||||
} else {
|
||||
setLocations([]);
|
||||
}
|
||||
}, [selectedWarehouse, loadLocations]);
|
||||
|
||||
/**
|
||||
* 공정 완료 시 마지막 공정인지 확인하고 창고 모달 표시
|
||||
* ResultPanel의 onSaved 또는 handleTimerAction("complete")에서 호출
|
||||
*/
|
||||
const checkLastProcessAndShowWarehouse = useCallback(async (completionData: Partial<ProcessTimerData>) => {
|
||||
if (!workOrderProcessId) return false;
|
||||
try {
|
||||
const res = await apiClient.get(`/pop/production/is-last-process/${workOrderProcessId}`);
|
||||
if (res.data?.success && res.data.data?.isLast) {
|
||||
const { targetWarehouseId, targetLocationCode } = res.data.data;
|
||||
// 이미 창고가 설정되어 있으면 모달 건너뛰기
|
||||
if (targetWarehouseId) {
|
||||
return false;
|
||||
}
|
||||
// 창고 목록 로드 후 모달 표시
|
||||
await loadWarehouses();
|
||||
setPendingCompletionData(completionData);
|
||||
setShowWarehouseModal(true);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// 확인 실패 시 모달 없이 진행
|
||||
}
|
||||
return false;
|
||||
}, [workOrderProcessId, loadWarehouses]);
|
||||
|
||||
const handleWarehouseConfirm = useCallback(async () => {
|
||||
if (!workOrderProcessId || !selectedWarehouse) {
|
||||
toast.error("창고를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setWarehouseSaving(true);
|
||||
try {
|
||||
// warehouse_code를 찾기 (API에서 warehouse_code로 저장하므로)
|
||||
const wh = warehouses.find((w) => w.id === selectedWarehouse);
|
||||
const warehouseCode = wh?.warehouse_code || selectedWarehouse;
|
||||
|
||||
await apiClient.post("/pop/production/update-target-warehouse", {
|
||||
work_order_process_id: workOrderProcessId,
|
||||
target_warehouse_id: warehouseCode,
|
||||
target_location_code: selectedLocation || null,
|
||||
});
|
||||
|
||||
// 기본 창고 설정 (로컬 스토리지)
|
||||
if (useDefaultWarehouse && wh) {
|
||||
localStorage.setItem("pop_default_warehouse", JSON.stringify({
|
||||
id: wh.id,
|
||||
warehouse_code: wh.warehouse_code,
|
||||
warehouse_name: wh.warehouse_name,
|
||||
location_code: selectedLocation || null,
|
||||
}));
|
||||
}
|
||||
|
||||
toast.success("입고 창고가 설정되었습니다.");
|
||||
setShowWarehouseModal(false);
|
||||
|
||||
// 보류 중이던 완료 데이터 반영
|
||||
if (pendingCompletionData) {
|
||||
setProcessData((prev) => prev ? { ...prev, ...pendingCompletionData } : prev);
|
||||
publish("process_completed", { workOrderProcessId, ...pendingCompletionData });
|
||||
setPendingCompletionData(null);
|
||||
}
|
||||
} catch {
|
||||
toast.error("창고 설정에 실패했습니다.");
|
||||
} finally {
|
||||
setWarehouseSaving(false);
|
||||
}
|
||||
}, [workOrderProcessId, selectedWarehouse, selectedLocation, useDefaultWarehouse, warehouses, pendingCompletionData, publish]);
|
||||
|
||||
// 모달 열릴 때 기본 창고 확인
|
||||
useEffect(() => {
|
||||
if (showWarehouseModal) {
|
||||
const saved = localStorage.getItem("pop_default_warehouse");
|
||||
if (saved) {
|
||||
try {
|
||||
const def = JSON.parse(saved);
|
||||
if (def.warehouse_code) {
|
||||
const found = warehouses.find((w) => w.warehouse_code === def.warehouse_code);
|
||||
if (found) {
|
||||
setSelectedWarehouse(found.id);
|
||||
if (def.location_code) setSelectedLocation(def.location_code);
|
||||
setUseDefaultWarehouse(true);
|
||||
}
|
||||
}
|
||||
} catch { /* 파싱 실패 무시 */ }
|
||||
}
|
||||
}
|
||||
}, [showWarehouseModal, warehouses]);
|
||||
|
||||
// ========================================
|
||||
// 좌측 사이드바 - 작업항목 그룹핑
|
||||
// ========================================
|
||||
|
|
@ -768,17 +906,23 @@ export function PopWorkDetailComponent({
|
|||
});
|
||||
const proc = (res.data?.[0] ?? null) as ProcessTimerData | null;
|
||||
if (proc) {
|
||||
setProcessData(proc);
|
||||
if (action === "complete") {
|
||||
toast.success("공정이 완료되었습니다.");
|
||||
publish("process_completed", { workOrderProcessId, goodQty, defectQty });
|
||||
// 마지막 공정이면 창고 선택 모달 표시
|
||||
const shown = await checkLastProcessAndShowWarehouse(proc);
|
||||
if (!shown) {
|
||||
setProcessData(proc);
|
||||
toast.success("공정이 완료되었습니다.");
|
||||
publish("process_completed", { workOrderProcessId, goodQty, defectQty });
|
||||
}
|
||||
} else {
|
||||
setProcessData(proc);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error("타이머 제어에 실패했습니다.");
|
||||
}
|
||||
},
|
||||
[workOrderProcessId, goodQty, defectQty, publish]
|
||||
[workOrderProcessId, goodQty, defectQty, publish, checkLastProcessAndShowWarehouse]
|
||||
);
|
||||
|
||||
// ========================================
|
||||
|
|
@ -1071,7 +1215,12 @@ export function PopWorkDetailComponent({
|
|||
sections={cfg.resultSections ?? []}
|
||||
isProcessCompleted={isProcessCompleted}
|
||||
defectTypes={cachedDefectTypes}
|
||||
onSaved={(updated) => {
|
||||
onSaved={async (updated) => {
|
||||
// 자동 완료 시 마지막 공정이면 창고 선택 모달 표시
|
||||
if (updated?.status === "completed") {
|
||||
const shown = await checkLastProcessAndShowWarehouse(updated);
|
||||
if (shown) return; // 모달이 뜨면 여기서 중단, 확인 후 반영
|
||||
}
|
||||
setProcessData((prev) => prev ? { ...prev, ...updated } : prev);
|
||||
publish("process_completed", { workOrderProcessId, status: updated?.status });
|
||||
}}
|
||||
|
|
@ -1324,6 +1473,94 @@ export function PopWorkDetailComponent({
|
|||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 창고 선택 모달 (마지막 공정 완료 시) */}
|
||||
<Dialog open={showWarehouseModal} onOpenChange={(open) => {
|
||||
if (!open && pendingCompletionData) {
|
||||
// 모달 닫기 시 보류 중이던 완료 데이터 반영 (창고 미선택)
|
||||
setProcessData((prev) => prev ? { ...prev, ...pendingCompletionData } : prev);
|
||||
publish("process_completed", { workOrderProcessId, ...pendingCompletionData });
|
||||
setPendingCompletionData(null);
|
||||
}
|
||||
setShowWarehouseModal(open);
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5 text-blue-600" />
|
||||
완제품 입고 창고 선택
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
마지막 공정이 완료되었습니다. 완제품을 입고할 창고를 선택해주세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">창고</label>
|
||||
<Select value={selectedWarehouse} onValueChange={setSelectedWarehouse}>
|
||||
<SelectTrigger className="h-12">
|
||||
<SelectValue placeholder="창고를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{warehouses.map((w) => (
|
||||
<SelectItem key={w.id} value={w.id}>
|
||||
{w.warehouse_name} ({w.warehouse_code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{locations.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">위치 (선택사항)</label>
|
||||
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
|
||||
<SelectTrigger className="h-12">
|
||||
<SelectValue placeholder="위치를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{locations.map((l) => (
|
||||
<SelectItem key={l.location_code} value={l.location_code}>
|
||||
{l.location_name} ({l.location_code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-2 pt-2">
|
||||
<Checkbox
|
||||
checked={useDefaultWarehouse}
|
||||
onCheckedChange={(v) => setUseDefaultWarehouse(v === true)}
|
||||
/>
|
||||
<span className="text-sm text-gray-600">항상 이 창고를 기본으로 사용</span>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowWarehouseModal(false);
|
||||
if (pendingCompletionData) {
|
||||
setProcessData((prev) => prev ? { ...prev, ...pendingCompletionData } : prev);
|
||||
publish("process_completed", { workOrderProcessId, ...pendingCompletionData });
|
||||
setPendingCompletionData(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
건너뛰기
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleWarehouseConfirm}
|
||||
disabled={!selectedWarehouse || warehouseSaving}
|
||||
>
|
||||
{warehouseSaving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
|
||||
확인
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue