|
|
@@ -21,6 +21,8 @@ import type { BomCombo, BomDetailResponse } from "@/app/api/bom"; |
|
|
import { fetchBomComboClient, fetchBomDetailClient } from "@/app/api/bom/client"; |
|
|
import { fetchBomComboClient, fetchBomDetailClient } from "@/app/api/bom/client"; |
|
|
import type { SelectChangeEvent } from "@mui/material/Select"; |
|
|
import type { SelectChangeEvent } from "@mui/material/Select"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
|
|
import { useMemo, useCallback } from "react"; |
|
|
const ImportBomDetailTab: React.FC = () => { |
|
|
const ImportBomDetailTab: React.FC = () => { |
|
|
const { t } = useTranslation( "common" ); |
|
|
const { t } = useTranslation( "common" ); |
|
|
const [bomList, setBomList] = useState<BomCombo[]>([]); |
|
|
const [bomList, setBomList] = useState<BomCombo[]>([]); |
|
|
@@ -28,7 +30,8 @@ const ImportBomDetailTab: React.FC = () => { |
|
|
const [detail, setDetail] = useState<BomDetailResponse | null>(null); |
|
|
const [detail, setDetail] = useState<BomDetailResponse | null>(null); |
|
|
const [loadingList, setLoadingList] = useState(false); |
|
|
const [loadingList, setLoadingList] = useState(false); |
|
|
const [loadingDetail, setLoadingDetail] = useState(false); |
|
|
const [loadingDetail, setLoadingDetail] = useState(false); |
|
|
|
|
|
|
|
|
|
|
|
const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([]) |
|
|
|
|
|
const [currentBom, setCurrentBom] = useState<BomCombo | null>(null); |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
const loadList = async () => { |
|
|
const loadList = async () => { |
|
|
setLoadingList(true); |
|
|
setLoadingList(true); |
|
|
@@ -41,7 +44,91 @@ const ImportBomDetailTab: React.FC = () => { |
|
|
}; |
|
|
}; |
|
|
loadList(); |
|
|
loadList(); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
type BomSearchKey = "code" | "name"; |
|
|
|
|
|
|
|
|
|
|
|
const searchCriteria: Criterion<BomSearchKey>[] = useMemo( |
|
|
|
|
|
() => [ |
|
|
|
|
|
{ label: t("Code"), paramName: "code", type: "text" }, |
|
|
|
|
|
{ label: t("Name"), paramName: "name", type: "text" }, |
|
|
|
|
|
], |
|
|
|
|
|
[t], |
|
|
|
|
|
); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
setFilteredBoms(bomList); // 初始顯示全部 |
|
|
|
|
|
}, [bomList]); |
|
|
|
|
|
const handleSearchBom = useCallback( |
|
|
|
|
|
(inputs: Record<BomSearchKey | `${BomSearchKey}To`, string>) => { |
|
|
|
|
|
const code = (inputs.code ?? "").trim().toLowerCase(); |
|
|
|
|
|
const name = (inputs.name ?? "").trim().toLowerCase(); |
|
|
|
|
|
|
|
|
|
|
|
const result = bomList.filter((b) => { |
|
|
|
|
|
const label = b.label.toLowerCase(); |
|
|
|
|
|
const okCode = !code || label.includes(code); |
|
|
|
|
|
const okName = !name || label.includes(name); |
|
|
|
|
|
return okCode && okName; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
setFilteredBoms(result); |
|
|
|
|
|
|
|
|
|
|
|
// 如果只找到一個,直接載入明細 |
|
|
|
|
|
if (result.length === 1) { |
|
|
|
|
|
const only = result[0]; |
|
|
|
|
|
setSelectedBomId(only.id); |
|
|
|
|
|
setDetail(null); |
|
|
|
|
|
setLoadingDetail(true); |
|
|
|
|
|
fetchBomDetailClient(only.id) |
|
|
|
|
|
.then((d) => setDetail(d)) |
|
|
|
|
|
.finally(() => setLoadingDetail(false)); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
[bomList], |
|
|
|
|
|
); |
|
|
|
|
|
const renderAllergic = (v?: number) => { |
|
|
|
|
|
if (v === 0) return "有"; |
|
|
|
|
|
if (v === 5) return "沒有"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderIsFloat = (v?: number) => { |
|
|
|
|
|
if (v === 5) return "沉"; |
|
|
|
|
|
if (v === 3) return "浮"; |
|
|
|
|
|
if (v === 0) return "不適用"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderIsDense = (v?: number) => { |
|
|
|
|
|
if (v === 5) return "淡"; |
|
|
|
|
|
if (v === 3) return "濃"; |
|
|
|
|
|
if (v === 0) return "不適用"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderTimeSequence = (v?: number) => { |
|
|
|
|
|
if (v === 5) return "上午"; |
|
|
|
|
|
if (v === 1) return "下午"; |
|
|
|
|
|
if (v === 0) return "不適用"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
const renderType = (v?: string) => { |
|
|
|
|
|
if (v === "FG") return "成品"; |
|
|
|
|
|
if (v === "WIP") return "半成品"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const renderComplexity = (v?: number) => { |
|
|
|
|
|
if (v === 10) return "簡單"; |
|
|
|
|
|
if (v === 5) return "中度"; |
|
|
|
|
|
if (v === 3) return "複雜"; |
|
|
|
|
|
if (v === 0) return "不適用"; |
|
|
|
|
|
return "-"; |
|
|
|
|
|
}; |
|
|
|
|
|
/* |
|
|
|
|
|
const handleResetBom = useCallback(() => { |
|
|
|
|
|
setFilteredBoms(bomList); |
|
|
|
|
|
setSelectedBomId(""); |
|
|
|
|
|
setDetail(null); |
|
|
|
|
|
}, [bomList]); |
|
|
|
|
|
*/ |
|
|
const handleChangeBom = async (event: SelectChangeEvent<number>) => { |
|
|
const handleChangeBom = async (event: SelectChangeEvent<number>) => { |
|
|
const id = Number(event.target.value); |
|
|
const id = Number(event.target.value); |
|
|
setSelectedBomId(id); |
|
|
setSelectedBomId(id); |
|
|
@@ -58,30 +145,22 @@ const ImportBomDetailTab: React.FC = () => { |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<Stack spacing={2}> |
|
|
<Stack spacing={2}> |
|
|
<FormControl size="small" sx={{ minWidth: 320 }}> |
|
|
|
|
|
<InputLabel id="import-bom-detail-select-label"> |
|
|
|
|
|
{t("Please Select BOM")} |
|
|
|
|
|
</InputLabel> |
|
|
|
|
|
<Select |
|
|
|
|
|
labelId="import-bom-detail-select-label" |
|
|
|
|
|
label="請選擇 BOM" |
|
|
|
|
|
value={selectedBomId} |
|
|
|
|
|
onChange={handleChangeBom} |
|
|
|
|
|
> |
|
|
|
|
|
{loadingList && ( |
|
|
|
|
|
<MenuItem value=""> |
|
|
|
|
|
<CircularProgress size={20} sx={{ mr: 1 }} /> 載入中… |
|
|
|
|
|
</MenuItem> |
|
|
|
|
|
)} |
|
|
|
|
|
{!loadingList && |
|
|
|
|
|
bomList.map((b) => ( |
|
|
|
|
|
<MenuItem key={b.id} value={b.id}> |
|
|
|
|
|
{b.label} |
|
|
|
|
|
</MenuItem> |
|
|
|
|
|
))} |
|
|
|
|
|
</Select> |
|
|
|
|
|
</FormControl> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<SearchBox<BomSearchKey> |
|
|
|
|
|
criteria={searchCriteria} |
|
|
|
|
|
onSearch={handleSearchBom} |
|
|
|
|
|
//onReset={handleResetBom} |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
{currentBom && ( |
|
|
|
|
|
<Paper variant="outlined" sx={{ p: 1.5 }}> |
|
|
|
|
|
<Typography variant="subtitle2"> |
|
|
|
|
|
CODE / NAME |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{currentBom.label} ({t("Output Quantity")} {currentBom.outputQty} {currentBom.outputQtyUom}) |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Paper> |
|
|
|
|
|
)} |
|
|
{loadingDetail && ( |
|
|
{loadingDetail && ( |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
{t("Loading BOM Detail...")} |
|
|
{t("Loading BOM Detail...")} |
|
|
@@ -90,15 +169,44 @@ const ImportBomDetailTab: React.FC = () => { |
|
|
|
|
|
|
|
|
{detail && ( |
|
|
{detail && ( |
|
|
<Stack spacing={2}> |
|
|
<Stack spacing={2}> |
|
|
<Typography variant="subtitle1"> |
|
|
|
|
|
{detail.itemCode} {detail.itemName}({t("Output Quantity")} {detail.outputQty}{" "} |
|
|
|
|
|
{detail.outputQtyUom}) |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
<Typography variant="subtitle1"> |
|
|
|
|
|
{detail.itemCode} {detail.itemName} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
{/* Basic Info 列表 */} |
|
|
|
|
|
<Paper variant="outlined" sx={{ p: 2 }}> |
|
|
|
|
|
<Typography variant="subtitle1" gutterBottom> |
|
|
|
|
|
{t("Basic Info")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
<Stack spacing={0.5}> |
|
|
|
|
|
{/* 第一行:輸出數量 + 類型 */} |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("Output Quantity")}:{" "} |
|
|
|
|
|
{detail.outputQty} {detail.outputQtyUom} |
|
|
|
|
|
{" "} |
|
|
|
|
|
{t("Type")}: {detail.description ?? "-"} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
{/* 第二行:各種指標,排成一行 key:value, key:value */} |
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("Allergic Substances")}: {renderAllergic(detail.allergicSubstances)} |
|
|
|
|
|
{" "}{t("Depth")}: {detail.isDark ?? "-"} |
|
|
|
|
|
{" "}{t("Float")}: {renderIsFloat(detail.isFloat)} |
|
|
|
|
|
{" "}{t("Density")}: {renderIsDense(detail.isDense)} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
<Typography variant="body2"> |
|
|
|
|
|
{t("Time Sequence")}: {renderTimeSequence(detail.timeSequence)} |
|
|
|
|
|
{" "}{t("Complexity")}: {renderComplexity(detail.complexity)} |
|
|
|
|
|
{" "}{t("Base Score")}: {detail.baseScore ?? "-"} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</Stack> |
|
|
|
|
|
</Paper> |
|
|
{/* 材料列表 */} |
|
|
{/* 材料列表 */} |
|
|
<Paper variant="outlined" sx={{ p: 2 }}> |
|
|
<Paper variant="outlined" sx={{ p: 2 }}> |
|
|
<Typography variant="subtitle1" gutterBottom> |
|
|
<Typography variant="subtitle1" gutterBottom> |
|
|
材料 (Bom Material) |
|
|
|
|
|
|
|
|
{t("Bom Material")} |
|
|
</Typography> |
|
|
</Typography> |
|
|
<Table size="small"> |
|
|
<Table size="small"> |
|
|
<TableHead> |
|
|
<TableHead> |
|
|
|