| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import React, { useEffect, useState } from "react"; | |||||
| import React, { useEffect, useRef, useState } from "react"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Stack, | Stack, | ||||
| @@ -32,7 +32,6 @@ import { | |||||
| type EquipmentMasterRow, | type EquipmentMasterRow, | ||||
| type ProcessMasterRow, | type ProcessMasterRow, | ||||
| } from "@/app/api/bom/client"; | } from "@/app/api/bom/client"; | ||||
| import type { SelectChangeEvent } from "@mui/material/Select"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { useMemo, useCallback } from "react"; | import { useMemo, useCallback } from "react"; | ||||
| @@ -70,6 +69,7 @@ const ImportBomDetailTab: React.FC = () => { | |||||
| const [loadingDetail, setLoadingDetail] = useState(false); | const [loadingDetail, setLoadingDetail] = useState(false); | ||||
| const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([]) | const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([]) | ||||
| const [currentBom, setCurrentBom] = useState<BomCombo | null>(null); | const [currentBom, setCurrentBom] = useState<BomCombo | null>(null); | ||||
| const loadDetailInFlightRef = useRef(false); | |||||
| type EditMaterialRow = { | type EditMaterialRow = { | ||||
| key: string; | key: string; | ||||
| @@ -195,15 +195,34 @@ const ImportBomDetailTab: React.FC = () => { | |||||
| [t], | [t], | ||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setFilteredBoms(bomList); // 初始顯示全部 | |||||
| setFilteredBoms([]); | |||||
| }, [bomList]); | }, [bomList]); | ||||
| const loadBomDetail = useCallback( | |||||
| async (id: number) => { | |||||
| if (!id || loadDetailInFlightRef.current) return; | |||||
| loadDetailInFlightRef.current = true; | |||||
| setSelectedBomId(id); | |||||
| setCurrentBom(bomList.find((b) => b.id === id) ?? null); | |||||
| setDetail(null); | |||||
| setLoadingDetail(true); | |||||
| try { | |||||
| const d = await fetchBomDetailClient(id); | |||||
| setDetail(d); | |||||
| } finally { | |||||
| setLoadingDetail(false); | |||||
| loadDetailInFlightRef.current = false; | |||||
| } | |||||
| }, | |||||
| [bomList], | |||||
| ); | |||||
| const handleSearchBom = useCallback( | const handleSearchBom = useCallback( | ||||
| (inputs: Record<BomSearchKey | `${BomSearchKey}To`, string>) => { | (inputs: Record<BomSearchKey | `${BomSearchKey}To`, string>) => { | ||||
| const code = (inputs.code ?? "").trim().toLowerCase(); | const code = (inputs.code ?? "").trim().toLowerCase(); | ||||
| const name = (inputs.name ?? "").trim().toLowerCase(); | const name = (inputs.name ?? "").trim().toLowerCase(); | ||||
| const result = bomList.filter((b) => { | const result = bomList.filter((b) => { | ||||
| const label = b.label.toLowerCase(); | |||||
| const label = String(b.label ?? "").toLowerCase(); | |||||
| const okCode = !code || label.includes(code); | const okCode = !code || label.includes(code); | ||||
| const okName = !name || label.includes(name); | const okName = !name || label.includes(name); | ||||
| return okCode && okName; | return okCode && okName; | ||||
| @@ -213,16 +232,14 @@ const ImportBomDetailTab: React.FC = () => { | |||||
| // 如果只找到一個,直接載入明細 | // 如果只找到一個,直接載入明細 | ||||
| if (result.length === 1) { | if (result.length === 1) { | ||||
| const only = result[0]; | |||||
| setSelectedBomId(only.id); | |||||
| void loadBomDetail(result[0].id); | |||||
| } else { | |||||
| setSelectedBomId(""); | |||||
| setCurrentBom(null); | |||||
| setDetail(null); | setDetail(null); | ||||
| setLoadingDetail(true); | |||||
| fetchBomDetailClient(only.id) | |||||
| .then((d) => setDetail(d)) | |||||
| .finally(() => setLoadingDetail(false)); | |||||
| } | } | ||||
| }, | }, | ||||
| [bomList], | |||||
| [bomList, loadBomDetail], | |||||
| ); | ); | ||||
| const renderAllergic = (v?: number) => { | const renderAllergic = (v?: number) => { | ||||
| if (v === 0) return "有"; | if (v === 0) return "有"; | ||||
| @@ -270,20 +287,6 @@ const ImportBomDetailTab: React.FC = () => { | |||||
| setDetail(null); | setDetail(null); | ||||
| }, [bomList]); | }, [bomList]); | ||||
| */ | */ | ||||
| const handleChangeBom = async (event: SelectChangeEvent<number>) => { | |||||
| const id = Number(event.target.value); | |||||
| setSelectedBomId(id); | |||||
| setDetail(null); | |||||
| if (!id) return; | |||||
| setLoadingDetail(true); | |||||
| try { | |||||
| const d = await fetchBomDetailClient(id); | |||||
| setDetail(d); | |||||
| } finally { | |||||
| setLoadingDetail(false); | |||||
| } | |||||
| }; | |||||
| const genKey = () => Math.random().toString(36).slice(2); | const genKey = () => Math.random().toString(36).slice(2); | ||||
| const startEdit = useCallback(async () => { | const startEdit = useCallback(async () => { | ||||
| @@ -555,16 +558,27 @@ const ImportBomDetailTab: React.FC = () => { | |||||
| //onReset={handleResetBom} | //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> | |||||
| )} | |||||
| {filteredBoms.length > 1 && ( | |||||
| <Paper variant="outlined" sx={{ p: 1.5 }}> | |||||
| <Typography variant="subtitle2" sx={{ mb: 1 }}> | |||||
| 找到多筆 BOM,請選擇一筆載入明細 | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={1} flexWrap="wrap"> | |||||
| {filteredBoms.map((b) => ( | |||||
| <Button | |||||
| key={b.id} | |||||
| size="small" | |||||
| variant={selectedBomId === b.id ? "contained" : "outlined"} | |||||
| disabled={loadingDetail} | |||||
| onClick={() => void loadBomDetail(b.id)} | |||||
| > | |||||
| {String(b.label ?? b.id)} ({renderType(b.description)}) | |||||
| </Button> | |||||
| ))} | |||||
| </Stack> | |||||
| </Paper> | |||||
| )} | |||||
| {loadingDetail && ( | {loadingDetail && ( | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Loading BOM Detail...")} | {t("Loading BOM Detail...")} | ||||