"use client"; import React, { useEffect, useRef, useState } from "react"; import { Box, Stack, Typography, FormControl, InputLabel, Select, MenuItem, CircularProgress, Paper, Table, TableHead, TableRow, TableCell, TableBody, Button, TextField, Checkbox, FormControlLabel, IconButton, Grid, } from "@mui/material"; import type { BomCombo, BomDetailResponse, BomStatus } from "@/app/api/bom"; import { editBomClient, saveBomAsNewVersionClient, fetchBomComboClient, fetchBomVersionsClient, fetchBomDetailClient, fetchAllEquipmentsMasterClient, fetchAllProcessesMasterClient, downloadBomDetailExcelClient, type EquipmentMasterRow, type ProcessMasterRow, } from "@/app/api/bom/client"; import { useTranslation } from "react-i18next"; import SearchBox, { Criterion } from "../SearchBox"; import { useMemo, useCallback } from "react"; import AddIcon from "@mui/icons-material/Add"; import SaveIcon from "@mui/icons-material/Save"; import CancelIcon from "@mui/icons-material/Cancel"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import { BomBasicInfoSection, type BomBasicInfoSectionHandle, saveBasicInfoDraft } from "./BomBasicInfoSection"; import { BomVersionControlBar } from "./BomVersionControlBar"; import { BomMaterialEditPanel, type BomMaterialEditPanelHandle, } from "./BomMaterialEditPanel"; import { BomProcessEditPanel, type BomProcessEditPanelHandle, } from "./BomProcessEditPanel"; import { getAmbiguousBomMatches, pickDefaultBom } from "./bomVersionUtils"; import { materialVersionLinesFromDetail } from "./bomVersionSaveUtils"; /** 以 description + "-" + name 對應 code,或同一筆設備的 description+name。 */ function resolveEquipmentCode( list: EquipmentMasterRow[], description: string, name: string, ): string | null { const d = description.trim(); const n = name.trim(); if (!d && !n) return null; if (!d || !n) return null; const composite = `${d}-${n}`; const byCode = list.find((e) => e.code === composite); if (byCode) return byCode.code; const byPair = list.find( (e) => e.description === d && e.name === n, ); return byPair?.code ?? null; } /** Full BOM field edit (materials/processes) — hidden until re-enabled. */ const SHOW_BOM_FULL_EDIT = false; type BomSearchKey = "code" | "name"; type BomSearchInputs = Record; const EMPTY_BOM_SEARCH_INPUTS: BomSearchInputs = { code: "", name: "", codeTo: "", nameTo: "", }; /** FP-MTMS Version Checklist | Functions Ref. No. 16 | v1.0.0 | 2026-07-16 */ const ImportBomDetailTab: React.FC = () => { const { t } = useTranslation(["importBom", "common"]); const [bomList, setBomList] = useState([]); const [selectedBomId, setSelectedBomId] = useState(""); const [detail, setDetail] = useState(null); const [loadingList, setLoadingList] = useState(false); const [versionsRefreshKey, setVersionsRefreshKey] = useState(0); const [filteredBoms, setFilteredBoms] = useState([]) const [currentBom, setCurrentBom] = useState(null); const loadDetailInFlightRef = useRef(false); const saveInFlightRef = useRef(false); const exportInFlightRef = useRef(false); const skipAutoSearchRef = useRef(false); const detailIdRef = useRef(null); const lastSearchInputsRef = useRef(EMPTY_BOM_SEARCH_INPUTS); type EditMaterialRow = { key: string; id?: number; itemCode?: string; itemName?: string; qty: number; isConsumable: boolean; baseUom?: string; stockQty?: number; stockUom?: string; salesQty?: number; salesUom?: string; }; type EditProcessRow = { key: string; id?: number; seqNo?: number; processCode?: string; processName?: string; description: string; /** 設備主檔 description(下拉),與 equipmentName 一併解析為 equipment.code */ equipmentDescription: string; equipmentName: string; durationInMinute: number; prepTimeInMinute: number; postProdTimeInMinute: number; }; const [isEditing, setIsEditing] = useState(false); const [editLoading, setEditLoading] = useState(false); const [editError, setEditError] = useState(null); const [editBasic, setEditBasic] = useState<{ description: string; outputQty: number; outputQtyUom: string; isDark: number; isFloat: number; isDense: number; scrapRate: number; allergicSubstances: number; timeSequence: number; complexity: number; isDrink: boolean; isPowderMixture: boolean; } | null>(null); const [editMaterials, setEditMaterials] = useState([]); const [editProcesses, setEditProcesses] = useState([]); const [equipmentMasterList, setEquipmentMasterList] = useState< EquipmentMasterRow[] >([]); const [processMasterList, setProcessMasterList] = useState< ProcessMasterRow[] >([]); const [editMasterLoading, setEditMasterLoading] = useState(false); const [saving, setSaving] = useState(false); const [exporting, setExporting] = useState(false); const [saveError, setSaveError] = useState(null); const [versionId, setVersionId] = useState(""); const [compareMode, setCompareMode] = useState(false); const [compareToId, setCompareToId] = useState(""); const [compareOldDetail, setCompareOldDetail] = useState(null); const materialEditRef = useRef(null); const processEditRef = useRef(null); const basicInfoRef = useRef(null); const [materialDirty, setMaterialDirty] = useState(false); const [processDirty, setProcessDirty] = useState(false); const [basicInfoDirty, setBasicInfoDirty] = useState(false); // Process add form (uses dropdown selections from master tables). const [processAddForm, setProcessAddForm] = useState<{ processCode: string; equipmentDescription: string; equipmentName: string; description: string; durationInMinute: number; prepTimeInMinute: number; postProdTimeInMinute: number; }>({ processCode: "", equipmentDescription: "", equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }); const processCodeOptions = useMemo(() => { const codes = new Set(); processMasterList.forEach((p) => { if (p.code) codes.add(p.code); }); return Array.from(codes).sort(); }, [processMasterList]); const equipmentDescriptionOptions = useMemo(() => { const s = new Set(); equipmentMasterList.forEach((e) => { if (e.description) s.add(e.description); }); return Array.from(s).sort(); }, [equipmentMasterList]); const equipmentNameOptions = useMemo(() => { const s = new Set(); equipmentMasterList.forEach((e) => { if (e.name) s.add(e.name); }); return Array.from(s).sort(); }, [equipmentMasterList]); useEffect(() => { const loadList = async () => { setLoadingList(true); try { const list = await fetchBomComboClient({ includeInactive: true }); setBomList(list); } finally { setLoadingList(false); } }; loadList(); }, []); const searchCriteria: Criterion[] = useMemo( () => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Name"), paramName: "name", type: "text" }, ], [t], ); useEffect(() => { setFilteredBoms([]); }, [bomList]); useEffect(() => { detailIdRef.current = detail?.id ?? null; }, [detail?.id]); const recomputeFilteredBoms = useCallback( (list: BomCombo[], inputs: BomSearchInputs) => { const code = (inputs.code ?? "").trim().toLowerCase(); const name = (inputs.name ?? "").trim().toLowerCase(); const matched = list.filter((b) => { const label = String(b.label ?? "").toLowerCase(); const okCode = !code || label.includes(code); const okName = !name || label.includes(name); return okCode && okName; }); setFilteredBoms(getAmbiguousBomMatches(matched)); return matched; }, [], ); const loadBomDetail = useCallback( async (id: number, options?: { resetVersionIds?: boolean }) => { if (!id || loadDetailInFlightRef.current) return; loadDetailInFlightRef.current = true; setSelectedBomId(id); setCurrentBom(bomList.find((b) => b.id === id) ?? null); try { const d = await fetchBomDetailClient(id); setDetail(d); setSaveError(null); if (options?.resetVersionIds !== false) { setVersionId(id); setCompareMode(false); setCompareToId(id); setCompareOldDetail(null); } } finally { loadDetailInFlightRef.current = false; } }, [bomList], ); useEffect(() => { if ( !compareMode || !compareToId || !versionId || compareToId === versionId ) { setCompareOldDetail(null); return; } let cancelled = false; void fetchBomDetailClient(compareToId).then((d) => { if (!cancelled) setCompareOldDetail(d); }); return () => { cancelled = true; }; }, [compareMode, compareToId, versionId]); const handleSearchBom = useCallback( (inputs: BomSearchInputs) => { lastSearchInputsRef.current = inputs; const code = (inputs.code ?? "").trim().toLowerCase(); const name = (inputs.name ?? "").trim().toLowerCase(); const matched = bomList.filter((b) => { const label = String(b.label ?? "").toLowerCase(); const okCode = !code || label.includes(code); const okName = !name || label.includes(name); return okCode && okName; }); const picked = pickDefaultBom(matched); setFilteredBoms(getAmbiguousBomMatches(matched)); if (picked) { if (picked.id === detailIdRef.current) return; void loadBomDetail(picked.id); } else if (matched.length === 0) { setSelectedBomId(""); setCurrentBom(null); setDetail(null); } else { setSelectedBomId(""); setCurrentBom(null); setDetail(null); } }, [bomList, loadBomDetail], ); useEffect(() => { if (bomList.length === 0 || skipAutoSearchRef.current) return; const inputs = lastSearchInputsRef.current; const hasQuery = Boolean((inputs.code ?? "").trim() || (inputs.name ?? "").trim()); if (!hasQuery) return; const code = (inputs.code ?? "").trim().toLowerCase(); const name = (inputs.name ?? "").trim().toLowerCase(); const matched = bomList.filter((b) => { const label = String(b.label ?? "").toLowerCase(); const okCode = !code || label.includes(code); const okName = !name || label.includes(name); return okCode && okName; }); const picked = pickDefaultBom(matched); setFilteredBoms(getAmbiguousBomMatches(matched)); if (!picked || picked.id === detailIdRef.current) return; void loadBomDetail(picked.id); }, [bomList, loadBomDetail]); const renderBomStatus = (v?: BomStatus | string) => { if (v === "active") return t("BOM Status Active"); if (v === "inactive") return t("BOM Status Inactive"); return "-"; }; const isComparingVersions = compareMode && Boolean(compareOldDetail) && compareToId !== "" && versionId !== "" && compareToId !== versionId; const renderType = (v?: string) => { if (v === "FG") return "成品"; if (v === "WIP") return "半成品"; return "-"; }; const saveDisabled = !materialDirty && !basicInfoDirty && !processDirty; const unresolvedMaterialIssueCount = useMemo( () => (detail?.materials ?? []).filter( (m) => (m.baseQty == null || m.stockQty == null) && (m.recipeQty ?? m.baseQty ?? 0) > 0, ).length, [detail?.materials], ); const nonMaterialDirty = basicInfoDirty || processDirty; const blockedByUnresolvedMaterialIssue = unresolvedMaterialIssueCount > 0 && nonMaterialDirty && !materialDirty; const unresolvedMaterialIssueMessage = unresolvedMaterialIssueCount > 0 ? t("bomSave_block_unresolvedMaterialIssue", { count: unresolvedMaterialIssueCount, }) : null; const isFgDetail = useMemo(() => { if (!detail) return false; const kind = (detail.bomKind ?? detail.description ?? "FG").trim().toUpperCase(); return kind !== "WIP"; }, [detail]); const putawayLocationMissing = isFgDetail && !(detail?.putawayLocationCode ?? "").trim(); const headerOutputQtyUnresolved = detail?.outputQtyStockConvertible === false; const putawayLocationMissingWarn = putawayLocationMissing ? t("bomPutawayLocation_missing_warn") : null; // Do not hard-disable Save purely from `detail` flags, // because user may still be editing draft values (putaway/header fix). // We enforce these blockers again in `handleSave()` using draft state. const effectiveSaveDisabled = saveDisabled || blockedByUnresolvedMaterialIssue; const refreshAfterSave = useCallback(async (saved: BomDetailResponse) => { const full = await fetchBomDetailClient(saved.id); detailIdRef.current = full.id; skipAutoSearchRef.current = true; setDetail(full); setVersionId(full.id); setCompareToId(full.id); setCompareMode(false); setCompareOldDetail(null); setVersionsRefreshKey((k) => k + 1); try { const list = await fetchBomComboClient({ includeInactive: true }); setBomList(list); setCurrentBom(list.find((b) => b.id === full.id) ?? null); const inputs = lastSearchInputsRef.current; const hasQuery = Boolean( (inputs.code ?? "").trim() || (inputs.name ?? "").trim(), ); if (hasQuery) { recomputeFilteredBoms(list, inputs); } } finally { skipAutoSearchRef.current = false; } }, [recomputeFilteredBoms]); const handleSave = useCallback(async () => { if (!detail?.id || saveInFlightRef.current) return; const materialPanel = materialEditRef.current; const processPanel = processEditRef.current; const basicPanel = basicInfoRef.current; const hasMaterialChanges = materialPanel?.hasMaterialChanges() ?? false; const hasProcessChanges = processPanel?.hasProcessChanges() ?? false; const hasBasicChanges = basicPanel?.hasBasicChanges() ?? false; const hasPutawayChanges = basicPanel?.hasPutawayChanges() ?? false; if (!hasMaterialChanges && !hasBasicChanges && !hasPutawayChanges && !hasProcessChanges) return; const putawayMissing = putawayLocationMissing && !hasPutawayChanges; if (putawayMissing) { setSaveError(t("bomPutawayLocation_missing_warn")); return; } const headerUnresolved = headerOutputQtyUnresolved; const headerDraft = basicPanel?.getDraft(); const headerChangedInDraft = headerDraft != null && (headerDraft.outputQty !== detail.outputQty || (headerDraft.outputQtyUom ?? "") !== (detail.outputQtyUom ?? "")); if (headerUnresolved && !headerChangedInDraft) { setSaveError(t("bomHeaderOutputQtyStockConvertFail_warn")); return; } if (unresolvedMaterialIssueCount > 0 && !hasMaterialChanges && (hasBasicChanges || hasProcessChanges || hasPutawayChanges)) { setSaveError( t("bomSave_block_unresolvedMaterialIssue", { count: unresolvedMaterialIssueCount, }), ); return; } const basicValidation = basicPanel?.validate() ?? null; if (basicValidation) { setSaveError(basicValidation); return; } const processValidation = (await processPanel?.validateAsync()) ?? null; if (processValidation) { setSaveError(processValidation); return; } saveInFlightRef.current = true; setSaving(true); setSaveError(null); try { let currentDetail = detail; let sourceDetail = detail; if (hasMaterialChanges || hasPutawayChanges || hasProcessChanges) { currentDetail = await saveBomAsNewVersionClient(detail.id, { materials: hasMaterialChanges ? materialPanel!.getMaterialSaveLines() : materialVersionLinesFromDetail(detail), ...(hasPutawayChanges ? { putawayLocationCode: basicPanel!.getPutawayLocationCodeForSave() ?? "", } : {}), ...(hasProcessChanges ? { processes: processPanel!.getProcessSaveLines() } : {}), }); materialPanel?.cancelEdit(); processPanel?.cancelEdit(); setMaterialDirty(false); setProcessDirty(false); if (hasPutawayChanges && !hasBasicChanges) { basicPanel?.cancelEdit(); setBasicInfoDirty(false); } sourceDetail = currentDetail; } if (hasBasicChanges) { const draft = basicPanel!.getDraft(); if (draft) { currentDetail = await saveBasicInfoDraft( currentDetail.id, sourceDetail, draft, ); basicPanel?.cancelEdit(); setBasicInfoDirty(false); } } await refreshAfterSave(currentDetail); } catch (e: unknown) { const message = e instanceof Error ? e.message : "Failed to save BOM"; setSaveError(message); } finally { setSaving(false); saveInFlightRef.current = false; } }, [ detail, headerOutputQtyUnresolved, putawayLocationMissing, refreshAfterSave, t, unresolvedMaterialIssueCount, ]); const handleExportExcel = useCallback(async () => { if (typeof versionId !== "number" || exportInFlightRef.current) return; exportInFlightRef.current = true; setExporting(true); setSaveError(null); try { const code = (detail?.itemCode ?? currentBom?.code ?? "BOM").trim() || "BOM"; const rev = detail?.revisionNo ?? 1; const { blob, fileName } = await downloadBomDetailExcelClient( versionId, `${code}_V${rev}.xlsx`, ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } catch (e: unknown) { setSaveError(e instanceof Error ? e.message : t("Export excel failed")); } finally { setExporting(false); exportInFlightRef.current = false; } }, [currentBom?.code, detail?.itemCode, detail?.revisionNo, t, versionId]); const handleToggleCompare = useCallback(async () => { if (compareMode) { setCompareMode(false); setCompareToId(versionId); setCompareOldDetail(null); return; } if (!currentBom?.code || versionId === "") return; const list = await fetchBomVersionsClient( currentBom.code, detail?.bomKind ?? detail?.description ?? "FG", ); const other = list.find((v) => v.id !== versionId); setCompareToId(other?.id ?? versionId); setCompareMode(true); }, [compareMode, currentBom?.code, detail?.bomKind, detail?.description, versionId]); /* const handleResetBom = useCallback(() => { setFilteredBoms(bomList); setSelectedBomId(""); setDetail(null); }, [bomList]); */ const genKey = () => Math.random().toString(36).slice(2); const startEdit = useCallback(async () => { if (!detail) return; setEditError(null); setEditMasterLoading(true); try { const [equipments, processes] = await Promise.all([ fetchAllEquipmentsMasterClient(), fetchAllProcessesMasterClient(), ]); setEquipmentMasterList(equipments); setProcessMasterList(processes); setEditBasic({ description: detail.description ?? "", outputQty: detail.outputQty ?? 0, outputQtyUom: detail.outputQtyUom ?? "", isDark: detail.isDark ?? 0, isFloat: detail.isFloat ?? 0, isDense: detail.isDense ?? 0, scrapRate: detail.scrapRate ?? 0, allergicSubstances: detail.allergicSubstances ?? 0, timeSequence: detail.timeSequence ?? 0, complexity: detail.complexity ?? 0, isDrink: detail.isDrink ?? false, isPowderMixture: detail.isPowderMixture ?? false, }); setEditMaterials( (detail.materials ?? []).map((m) => ({ key: genKey(), id: undefined, itemCode: m.itemCode ?? "", itemName: m.itemName ?? "", qty: m.baseQty ?? 0, isConsumable: m.isConsumable ?? false, baseUom: m.baseUom, stockQty: m.stockQty, stockUom: m.stockUom, salesQty: m.salesQty, salesUom: m.salesUom, })), ); setEditProcesses( (detail.processes ?? []).map((p) => { const code = (p.equipmentCode ?? "").trim(); const eq = code ? equipments.find((e) => e.code === code) : undefined; return { key: genKey(), id: undefined, seqNo: p.seqNo, processCode: p.processCode ?? "", processName: p.processName, description: p.processDescription ?? "", equipmentDescription: eq?.description ?? "", equipmentName: eq?.name ?? "", durationInMinute: p.durationInMinute ?? 0, prepTimeInMinute: p.prepTimeInMinute ?? 0, postProdTimeInMinute: p.postProdTimeInMinute ?? 0, }; }), ); setIsEditing(true); } catch (e: unknown) { const msg = e && typeof e === "object" && "message" in e ? String((e as { message?: string }).message) : "載入製程/設備主檔失敗"; setEditError(msg); } finally { setEditMasterLoading(false); } }, [detail]); const cancelEdit = useCallback(() => { setIsEditing(false); setEditLoading(false); setEditError(null); setEditBasic(null); setEditMaterials([]); setEditProcesses([]); setProcessAddForm({ processCode: "", equipmentDescription: "", equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }); setEquipmentMasterList([]); setProcessMasterList([]); }, []); const addMaterialRow = useCallback(() => { setEditMaterials((prev) => [ ...prev, { key: genKey(), itemCode: "", itemName: "", qty: 0, isConsumable: false, baseUom: "", stockQty: undefined, stockUom: "", salesQty: undefined, salesUom: "", }, ]); }, []); const addProcessRow = useCallback(() => { setEditProcesses((prev) => [ ...prev, { key: genKey(), seqNo: undefined, processCode: "", processName: "", description: "", equipmentDescription: "", equipmentName: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }, ]); }, []); const addProcessFromForm = useCallback(() => { const pCode = processAddForm.processCode.trim(); if (!pCode) { setEditError("請先選擇工序 Process Code"); return; } const ed = processAddForm.equipmentDescription.trim(); const en = processAddForm.equipmentName.trim(); if ((ed && !en) || (!ed && en)) { setEditError("設備描述與名稱需同時選取,或同時留空(不適用)"); return; } if (ed && en) { const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); if (!resolved) { setEditError( `設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`, ); return; } } setEditProcesses((prev) => [ ...prev, { key: genKey(), seqNo: undefined, processCode: pCode, processName: "", description: processAddForm.description ?? "", equipmentDescription: ed, equipmentName: en, durationInMinute: processAddForm.durationInMinute ?? 0, prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0, postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0, }, ]); setProcessAddForm({ processCode: "", equipmentDescription: "", equipmentName: "", description: "", durationInMinute: 0, prepTimeInMinute: 0, postProdTimeInMinute: 0, }); setEditError(null); }, [processAddForm, equipmentMasterList]); const deleteMaterialRow = useCallback((key: string) => { setEditMaterials((prev) => prev.filter((r) => r.key !== key)); }, []); const deleteProcessRow = useCallback((key: string) => { setEditProcesses((prev) => prev.filter((r) => r.key !== key)); }, []); const handleSaveEdit = useCallback(async () => { if (!detail || !editBasic) return; setEditLoading(true); setEditError(null); try { for (const p of editProcesses) { if (!p.processCode?.trim()) { throw new Error("工序行 Process Code 不能为空"); } const ed = p.equipmentDescription.trim(); const en = p.equipmentName.trim(); if ((ed && !en) || (!ed && en)) { throw new Error("各製程行的設備描述與名稱需同時填寫或同時留空"); } if (ed && en) { const resolved = resolveEquipmentCode(equipmentMasterList, ed, en); if (!resolved) { throw new Error( `設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`, ); } } } const payload: any = { description: editBasic.description || undefined, outputQty: editBasic.outputQty, outputQtyUom: editBasic.outputQtyUom || undefined, isDark: editBasic.isDark, isFloat: editBasic.isFloat, isDense: editBasic.isDense, scrapRate: editBasic.scrapRate, allergicSubstances: editBasic.allergicSubstances, timeSequence: editBasic.timeSequence, complexity: editBasic.complexity, isDrink: editBasic.isDrink, isPowderMixture: editBasic.isPowderMixture, processes: editProcesses.map((p) => { const ed = p.equipmentDescription.trim(); const en = p.equipmentName.trim(); const equipmentCode = ed && en ? resolveEquipmentCode(equipmentMasterList, ed, en) ?? undefined : undefined; return { id: p.id, seqNo: p.seqNo, processCode: p.processCode?.trim() || undefined, equipmentCode, description: p.description || undefined, durationInMinute: p.durationInMinute, prepTimeInMinute: p.prepTimeInMinute, postProdTimeInMinute: p.postProdTimeInMinute, }; }), }; const updated = await editBomClient(detail.id, payload); setDetail(updated); setIsEditing(false); } catch (e: any) { setEditError(e?.message || "保存失败"); } finally { setEditLoading(false); } }, [detail, editBasic, editProcesses, equipmentMasterList]); return ( criteria={searchCriteria} onSearch={handleSearchBom} //onReset={handleResetBom} /> {filteredBoms.length > 1 && ( 找到多筆 BOM,請選擇一筆載入明細 {filteredBoms.map((b) => ( ))} )} {detail && ( {detail.itemCode} {detail.itemName} {(currentBom?.code ?? detail.itemCode) && ( { setVersionId(id); void loadBomDetail(id, { resetVersionIds: false }); }} onToggleCompare={() => void handleToggleCompare()} onCompareToChange={setCompareToId} onExport={() => void handleExportExcel()} onSave={() => void handleSave()} /> )} {unresolvedMaterialIssueMessage && ( {unresolvedMaterialIssueMessage} )} {putawayLocationMissingWarn && ( {putawayLocationMissingWarn} )} )} ); }; export default ImportBomDetailTab;