FPSMS-frontend
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

ImportBomDetailTab.tsx 41 KiB

pirms 2 mēnešiem
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 2 mēnešiem
pirms 1 mēnesi
pirms 1 mēnesi
pirms 1 mēnesi
pirms 2 mēnešiem
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. "use client";
  2. import React, { useEffect, useRef, useState } from "react";
  3. import {
  4. Box,
  5. Stack,
  6. Typography,
  7. FormControl,
  8. InputLabel,
  9. Select,
  10. MenuItem,
  11. CircularProgress,
  12. Paper,
  13. Table,
  14. TableHead,
  15. TableRow,
  16. TableCell,
  17. TableBody,
  18. Button,
  19. TextField,
  20. Checkbox,
  21. FormControlLabel,
  22. IconButton,
  23. } from "@mui/material";
  24. import type { BomCombo, BomDetailResponse } from "@/app/api/bom";
  25. import {
  26. editBomClient,
  27. fetchBomComboClient,
  28. fetchBomDetailClient,
  29. fetchAllEquipmentsMasterClient,
  30. fetchAllProcessesMasterClient,
  31. type EquipmentMasterRow,
  32. type ProcessMasterRow,
  33. } from "@/app/api/bom/client";
  34. import { useTranslation } from "react-i18next";
  35. import SearchBox, { Criterion } from "../SearchBox";
  36. import { useMemo, useCallback } from "react";
  37. import AddIcon from "@mui/icons-material/Add";
  38. import SaveIcon from "@mui/icons-material/Save";
  39. import CancelIcon from "@mui/icons-material/Cancel";
  40. import DeleteIcon from "@mui/icons-material/Delete";
  41. import EditIcon from "@mui/icons-material/Edit";
  42. /** 以 description + "-" + name 對應 code,或同一筆設備的 description+name。 */
  43. function resolveEquipmentCode(
  44. list: EquipmentMasterRow[],
  45. description: string,
  46. name: string,
  47. ): string | null {
  48. const d = description.trim();
  49. const n = name.trim();
  50. if (!d && !n) return null;
  51. if (!d || !n) return null;
  52. const composite = `${d}-${n}`;
  53. const byCode = list.find((e) => e.code === composite);
  54. if (byCode) return byCode.code;
  55. const byPair = list.find(
  56. (e) => e.description === d && e.name === n,
  57. );
  58. return byPair?.code ?? null;
  59. }
  60. const ImportBomDetailTab: React.FC = () => {
  61. const { t } = useTranslation( "common" );
  62. const [bomList, setBomList] = useState<BomCombo[]>([]);
  63. const [selectedBomId, setSelectedBomId] = useState<number | "">("");
  64. const [detail, setDetail] = useState<BomDetailResponse | null>(null);
  65. const [loadingList, setLoadingList] = useState(false);
  66. const [loadingDetail, setLoadingDetail] = useState(false);
  67. const [filteredBoms, setFilteredBoms] = useState<BomCombo[]>([])
  68. const [currentBom, setCurrentBom] = useState<BomCombo | null>(null);
  69. const loadDetailInFlightRef = useRef(false);
  70. type EditMaterialRow = {
  71. key: string;
  72. id?: number;
  73. itemCode?: string;
  74. itemName?: string;
  75. qty: number;
  76. isConsumable: boolean;
  77. baseUom?: string;
  78. stockQty?: number;
  79. stockUom?: string;
  80. salesQty?: number;
  81. salesUom?: string;
  82. };
  83. type EditProcessRow = {
  84. key: string;
  85. id?: number;
  86. seqNo?: number;
  87. processCode?: string;
  88. processName?: string;
  89. description: string;
  90. /** 設備主檔 description(下拉),與 equipmentName 一併解析為 equipment.code */
  91. equipmentDescription: string;
  92. equipmentName: string;
  93. durationInMinute: number;
  94. prepTimeInMinute: number;
  95. postProdTimeInMinute: number;
  96. };
  97. const [isEditing, setIsEditing] = useState(false);
  98. const [editLoading, setEditLoading] = useState(false);
  99. const [editError, setEditError] = useState<string | null>(null);
  100. const [editBasic, setEditBasic] = useState<{
  101. description: string;
  102. outputQty: number;
  103. outputQtyUom: string;
  104. isDark: number;
  105. isFloat: number;
  106. isDense: number;
  107. scrapRate: number;
  108. allergicSubstances: number;
  109. timeSequence: number;
  110. complexity: number;
  111. isDrink: boolean;
  112. } | null>(null);
  113. const [editMaterials, setEditMaterials] = useState<EditMaterialRow[]>([]);
  114. const [editProcesses, setEditProcesses] = useState<EditProcessRow[]>([]);
  115. const [equipmentMasterList, setEquipmentMasterList] = useState<
  116. EquipmentMasterRow[]
  117. >([]);
  118. const [processMasterList, setProcessMasterList] = useState<
  119. ProcessMasterRow[]
  120. >([]);
  121. const [editMasterLoading, setEditMasterLoading] = useState(false);
  122. // Process add form (uses dropdown selections from master tables).
  123. const [processAddForm, setProcessAddForm] = useState<{
  124. processCode: string;
  125. equipmentDescription: string;
  126. equipmentName: string;
  127. description: string;
  128. durationInMinute: number;
  129. prepTimeInMinute: number;
  130. postProdTimeInMinute: number;
  131. }>({
  132. processCode: "",
  133. equipmentDescription: "",
  134. equipmentName: "",
  135. description: "",
  136. durationInMinute: 0,
  137. prepTimeInMinute: 0,
  138. postProdTimeInMinute: 0,
  139. });
  140. const processCodeOptions = useMemo(() => {
  141. const codes = new Set<string>();
  142. processMasterList.forEach((p) => {
  143. if (p.code) codes.add(p.code);
  144. });
  145. return Array.from(codes).sort();
  146. }, [processMasterList]);
  147. const equipmentDescriptionOptions = useMemo(() => {
  148. const s = new Set<string>();
  149. equipmentMasterList.forEach((e) => {
  150. if (e.description) s.add(e.description);
  151. });
  152. return Array.from(s).sort();
  153. }, [equipmentMasterList]);
  154. const equipmentNameOptions = useMemo(() => {
  155. const s = new Set<string>();
  156. equipmentMasterList.forEach((e) => {
  157. if (e.name) s.add(e.name);
  158. });
  159. return Array.from(s).sort();
  160. }, [equipmentMasterList]);
  161. useEffect(() => {
  162. const loadList = async () => {
  163. setLoadingList(true);
  164. try {
  165. const list = await fetchBomComboClient();
  166. setBomList(list);
  167. } finally {
  168. setLoadingList(false);
  169. }
  170. };
  171. loadList();
  172. }, []);
  173. type BomSearchKey = "code" | "name";
  174. const searchCriteria: Criterion<BomSearchKey>[] = useMemo(
  175. () => [
  176. { label: t("Code"), paramName: "code", type: "text" },
  177. { label: t("Name"), paramName: "name", type: "text" },
  178. ],
  179. [t],
  180. );
  181. useEffect(() => {
  182. setFilteredBoms([]);
  183. }, [bomList]);
  184. const loadBomDetail = useCallback(
  185. async (id: number) => {
  186. if (!id || loadDetailInFlightRef.current) return;
  187. loadDetailInFlightRef.current = true;
  188. setSelectedBomId(id);
  189. setCurrentBom(bomList.find((b) => b.id === id) ?? null);
  190. setDetail(null);
  191. setLoadingDetail(true);
  192. try {
  193. const d = await fetchBomDetailClient(id);
  194. setDetail(d);
  195. } finally {
  196. setLoadingDetail(false);
  197. loadDetailInFlightRef.current = false;
  198. }
  199. },
  200. [bomList],
  201. );
  202. const handleSearchBom = useCallback(
  203. (inputs: Record<BomSearchKey | `${BomSearchKey}To`, string>) => {
  204. const code = (inputs.code ?? "").trim().toLowerCase();
  205. const name = (inputs.name ?? "").trim().toLowerCase();
  206. const result = bomList.filter((b) => {
  207. const label = String(b.label ?? "").toLowerCase();
  208. const okCode = !code || label.includes(code);
  209. const okName = !name || label.includes(name);
  210. return okCode && okName;
  211. });
  212. setFilteredBoms(result);
  213. // 如果只找到一個,直接載入明細
  214. if (result.length === 1) {
  215. void loadBomDetail(result[0].id);
  216. } else {
  217. setSelectedBomId("");
  218. setCurrentBom(null);
  219. setDetail(null);
  220. }
  221. },
  222. [bomList, loadBomDetail],
  223. );
  224. const renderAllergic = (v?: number) => {
  225. if (v === 0) return "有";
  226. if (v === 5) return "沒有";
  227. return "-";
  228. };
  229. const renderIsFloat = (v?: number) => {
  230. if (v === 5) return "沉";
  231. if (v === 3) return "浮";
  232. if (v === 0) return "不適用";
  233. return "-";
  234. };
  235. const renderIsDense = (v?: number) => {
  236. if (v === 5) return "淡";
  237. if (v === 3) return "濃";
  238. if (v === 0) return "不適用";
  239. return "-";
  240. };
  241. const renderTimeSequence = (v?: number) => {
  242. if (v === 5) return "上午";
  243. if (v === 1) return "下午";
  244. if (v === 0) return "不適用";
  245. return "-";
  246. };
  247. const renderType = (v?: string) => {
  248. if (v === "FG") return "成品";
  249. if (v === "WIP") return "半成品";
  250. return "-";
  251. };
  252. const renderComplexity = (v?: number) => {
  253. if (v === 10) return "簡單";
  254. if (v === 5) return "中度";
  255. if (v === 3) return "複雜";
  256. if (v === 0) return "不適用";
  257. return "-";
  258. };
  259. /*
  260. const handleResetBom = useCallback(() => {
  261. setFilteredBoms(bomList);
  262. setSelectedBomId("");
  263. setDetail(null);
  264. }, [bomList]);
  265. */
  266. const genKey = () => Math.random().toString(36).slice(2);
  267. const startEdit = useCallback(async () => {
  268. if (!detail) return;
  269. setEditError(null);
  270. setEditMasterLoading(true);
  271. try {
  272. const [equipments, processes] = await Promise.all([
  273. fetchAllEquipmentsMasterClient(),
  274. fetchAllProcessesMasterClient(),
  275. ]);
  276. setEquipmentMasterList(equipments);
  277. setProcessMasterList(processes);
  278. setEditBasic({
  279. description: detail.description ?? "",
  280. outputQty: detail.outputQty ?? 0,
  281. outputQtyUom: detail.outputQtyUom ?? "",
  282. isDark: detail.isDark ?? 0,
  283. isFloat: detail.isFloat ?? 0,
  284. isDense: detail.isDense ?? 0,
  285. scrapRate: detail.scrapRate ?? 0,
  286. allergicSubstances: detail.allergicSubstances ?? 0,
  287. timeSequence: detail.timeSequence ?? 0,
  288. complexity: detail.complexity ?? 0,
  289. isDrink: detail.isDrink ?? false,
  290. });
  291. setEditMaterials(
  292. (detail.materials ?? []).map((m) => ({
  293. key: genKey(),
  294. id: undefined,
  295. itemCode: m.itemCode ?? "",
  296. itemName: m.itemName ?? "",
  297. qty: m.baseQty ?? 0,
  298. isConsumable: m.isConsumable ?? false,
  299. baseUom: m.baseUom,
  300. stockQty: m.stockQty,
  301. stockUom: m.stockUom,
  302. salesQty: m.salesQty,
  303. salesUom: m.salesUom,
  304. })),
  305. );
  306. setEditProcesses(
  307. (detail.processes ?? []).map((p) => {
  308. const code = (p.equipmentCode ?? "").trim();
  309. const eq = code
  310. ? equipments.find((e) => e.code === code)
  311. : undefined;
  312. return {
  313. key: genKey(),
  314. id: undefined,
  315. seqNo: p.seqNo,
  316. processCode: p.processCode ?? "",
  317. processName: p.processName,
  318. description: p.processDescription ?? "",
  319. equipmentDescription: eq?.description ?? "",
  320. equipmentName: eq?.name ?? "",
  321. durationInMinute: p.durationInMinute ?? 0,
  322. prepTimeInMinute: p.prepTimeInMinute ?? 0,
  323. postProdTimeInMinute: p.postProdTimeInMinute ?? 0,
  324. };
  325. }),
  326. );
  327. setIsEditing(true);
  328. } catch (e: unknown) {
  329. const msg =
  330. e && typeof e === "object" && "message" in e
  331. ? String((e as { message?: string }).message)
  332. : "載入製程/設備主檔失敗";
  333. setEditError(msg);
  334. } finally {
  335. setEditMasterLoading(false);
  336. }
  337. }, [detail]);
  338. const cancelEdit = useCallback(() => {
  339. setIsEditing(false);
  340. setEditLoading(false);
  341. setEditError(null);
  342. setEditBasic(null);
  343. setEditMaterials([]);
  344. setEditProcesses([]);
  345. setProcessAddForm({
  346. processCode: "",
  347. equipmentDescription: "",
  348. equipmentName: "",
  349. description: "",
  350. durationInMinute: 0,
  351. prepTimeInMinute: 0,
  352. postProdTimeInMinute: 0,
  353. });
  354. setEquipmentMasterList([]);
  355. setProcessMasterList([]);
  356. }, []);
  357. const addMaterialRow = useCallback(() => {
  358. setEditMaterials((prev) => [
  359. ...prev,
  360. {
  361. key: genKey(),
  362. itemCode: "",
  363. itemName: "",
  364. qty: 0,
  365. isConsumable: false,
  366. baseUom: "",
  367. stockQty: undefined,
  368. stockUom: "",
  369. salesQty: undefined,
  370. salesUom: "",
  371. },
  372. ]);
  373. }, []);
  374. const addProcessRow = useCallback(() => {
  375. setEditProcesses((prev) => [
  376. ...prev,
  377. {
  378. key: genKey(),
  379. seqNo: undefined,
  380. processCode: "",
  381. processName: "",
  382. description: "",
  383. equipmentDescription: "",
  384. equipmentName: "",
  385. durationInMinute: 0,
  386. prepTimeInMinute: 0,
  387. postProdTimeInMinute: 0,
  388. },
  389. ]);
  390. }, []);
  391. const addProcessFromForm = useCallback(() => {
  392. const pCode = processAddForm.processCode.trim();
  393. if (!pCode) {
  394. setEditError("請先選擇工序 Process Code");
  395. return;
  396. }
  397. const ed = processAddForm.equipmentDescription.trim();
  398. const en = processAddForm.equipmentName.trim();
  399. if ((ed && !en) || (!ed && en)) {
  400. setEditError("設備描述與名稱需同時選取,或同時留空(不適用)");
  401. return;
  402. }
  403. if (ed && en) {
  404. const resolved = resolveEquipmentCode(equipmentMasterList, ed, en);
  405. if (!resolved) {
  406. setEditError(
  407. `設備組合「${ed}-${en}」在主檔中找不到對應設備代碼,請確認後再試`,
  408. );
  409. return;
  410. }
  411. }
  412. setEditProcesses((prev) => [
  413. ...prev,
  414. {
  415. key: genKey(),
  416. seqNo: undefined,
  417. processCode: pCode,
  418. processName: "",
  419. description: processAddForm.description ?? "",
  420. equipmentDescription: ed,
  421. equipmentName: en,
  422. durationInMinute: processAddForm.durationInMinute ?? 0,
  423. prepTimeInMinute: processAddForm.prepTimeInMinute ?? 0,
  424. postProdTimeInMinute: processAddForm.postProdTimeInMinute ?? 0,
  425. },
  426. ]);
  427. setProcessAddForm({
  428. processCode: "",
  429. equipmentDescription: "",
  430. equipmentName: "",
  431. description: "",
  432. durationInMinute: 0,
  433. prepTimeInMinute: 0,
  434. postProdTimeInMinute: 0,
  435. });
  436. setEditError(null);
  437. }, [processAddForm, equipmentMasterList]);
  438. const deleteMaterialRow = useCallback((key: string) => {
  439. setEditMaterials((prev) => prev.filter((r) => r.key !== key));
  440. }, []);
  441. const deleteProcessRow = useCallback((key: string) => {
  442. setEditProcesses((prev) => prev.filter((r) => r.key !== key));
  443. }, []);
  444. const handleSaveEdit = useCallback(async () => {
  445. if (!detail || !editBasic) return;
  446. setEditLoading(true);
  447. setEditError(null);
  448. try {
  449. for (const p of editProcesses) {
  450. if (!p.processCode?.trim()) {
  451. throw new Error("工序行 Process Code 不能为空");
  452. }
  453. const ed = p.equipmentDescription.trim();
  454. const en = p.equipmentName.trim();
  455. if ((ed && !en) || (!ed && en)) {
  456. throw new Error("各製程行的設備描述與名稱需同時填寫或同時留空");
  457. }
  458. if (ed && en) {
  459. const resolved = resolveEquipmentCode(equipmentMasterList, ed, en);
  460. if (!resolved) {
  461. throw new Error(
  462. `設備「${ed}-${en}」在主檔中無對應設備代碼,請修正後再儲存`,
  463. );
  464. }
  465. }
  466. }
  467. const payload: any = {
  468. description: editBasic.description || undefined,
  469. outputQty: editBasic.outputQty,
  470. outputQtyUom: editBasic.outputQtyUom || undefined,
  471. isDark: editBasic.isDark,
  472. isFloat: editBasic.isFloat,
  473. isDense: editBasic.isDense,
  474. scrapRate: editBasic.scrapRate,
  475. allergicSubstances: editBasic.allergicSubstances,
  476. timeSequence: editBasic.timeSequence,
  477. complexity: editBasic.complexity,
  478. isDrink: editBasic.isDrink,
  479. processes: editProcesses.map((p) => {
  480. const ed = p.equipmentDescription.trim();
  481. const en = p.equipmentName.trim();
  482. const equipmentCode =
  483. ed && en
  484. ? resolveEquipmentCode(equipmentMasterList, ed, en) ?? undefined
  485. : undefined;
  486. return {
  487. id: p.id,
  488. seqNo: p.seqNo,
  489. processCode: p.processCode?.trim() || undefined,
  490. equipmentCode,
  491. description: p.description || undefined,
  492. durationInMinute: p.durationInMinute,
  493. prepTimeInMinute: p.prepTimeInMinute,
  494. postProdTimeInMinute: p.postProdTimeInMinute,
  495. };
  496. }),
  497. };
  498. const updated = await editBomClient(detail.id, payload);
  499. setDetail(updated);
  500. setIsEditing(false);
  501. } catch (e: any) {
  502. setEditError(e?.message || "保存失败");
  503. } finally {
  504. setEditLoading(false);
  505. }
  506. }, [detail, editBasic, editProcesses, equipmentMasterList]);
  507. return (
  508. <Stack spacing={2}>
  509. <SearchBox<BomSearchKey>
  510. criteria={searchCriteria}
  511. onSearch={handleSearchBom}
  512. //onReset={handleResetBom}
  513. />
  514. {filteredBoms.length > 1 && (
  515. <Paper variant="outlined" sx={{ p: 1.5 }}>
  516. <Typography variant="subtitle2" sx={{ mb: 1 }}>
  517. 找到多筆 BOM,請選擇一筆載入明細
  518. </Typography>
  519. <Stack direction="row" spacing={1} flexWrap="wrap">
  520. {filteredBoms.map((b) => (
  521. <Button
  522. key={b.id}
  523. size="small"
  524. variant={selectedBomId === b.id ? "contained" : "outlined"}
  525. disabled={loadingDetail}
  526. onClick={() => void loadBomDetail(b.id)}
  527. >
  528. {String(b.label ?? b.id)} ({renderType(b.description)})
  529. </Button>
  530. ))}
  531. </Stack>
  532. </Paper>
  533. )}
  534. {loadingDetail && (
  535. <Typography variant="body2" color="text.secondary">
  536. {t("Loading BOM Detail...")}
  537. </Typography>
  538. )}
  539. {detail && (
  540. <Stack spacing={2}>
  541. <Typography variant="subtitle1">
  542. {detail.itemCode} {detail.itemName}
  543. </Typography>
  544. {/* Basic Info 列表 */}
  545. <Paper variant="outlined" sx={{ p: 2 }}>
  546. <Stack
  547. direction="row"
  548. alignItems="center"
  549. justifyContent="space-between"
  550. sx={{ mb: 1 }}
  551. >
  552. <Typography variant="subtitle1" gutterBottom>
  553. {t("Basic Info")}
  554. </Typography>
  555. {!isEditing ? (
  556. <Button
  557. size="small"
  558. startIcon={
  559. editMasterLoading ? (
  560. <CircularProgress size={16} />
  561. ) : (
  562. <EditIcon />
  563. )
  564. }
  565. variant="outlined"
  566. onClick={() => void startEdit()}
  567. disabled={editMasterLoading}
  568. >
  569. {editMasterLoading ? t("Loading...") : t("Edit")}
  570. </Button>
  571. ) : (
  572. <Stack direction="row" spacing={1}>
  573. <Button
  574. size="small"
  575. startIcon={<SaveIcon />}
  576. variant="contained"
  577. disabled={editLoading}
  578. onClick={handleSaveEdit}
  579. >
  580. {editLoading ? t("Saving...") : t("Save")}
  581. </Button>
  582. <Button
  583. size="small"
  584. startIcon={<CancelIcon />}
  585. variant="outlined"
  586. disabled={editLoading}
  587. onClick={cancelEdit}
  588. >
  589. {t("Cancel")}
  590. </Button>
  591. </Stack>
  592. )}
  593. </Stack>
  594. {editError && (
  595. <Typography variant="body2" color="error">
  596. {editError}
  597. </Typography>
  598. )}
  599. {!isEditing && (
  600. <Stack spacing={0.5}>
  601. {/* 第一行:輸出數量 + 類型 */}
  602. <Typography variant="body2">
  603. {t("Output Quantity")}: {detail.outputQty} {detail.outputQtyUom}
  604. {" "}
  605. {t("Type")}: {detail.description ?? "-"}
  606. </Typography>
  607. {/* 第二行:各種指標,排成一行 key:value, key:value */}
  608. <Typography variant="body2">
  609. {t("Allergic Substances")}:{" "}
  610. {renderAllergic(detail.allergicSubstances)}
  611. {" "}{t("Depth")}: {detail.isDark ?? "-"}
  612. {" "}{t("Float")}: {renderIsFloat(detail.isFloat)}
  613. {" "}{t("Density")}: {renderIsDense(detail.isDense)}
  614. </Typography>
  615. <Typography variant="body2">
  616. {t("Time Sequence")}: {renderTimeSequence(detail.timeSequence)}
  617. {" "}{t("Complexity")}: {renderComplexity(detail.complexity)}
  618. {" "}{t("Base Score")}: {detail.baseScore ?? "-"}
  619. </Typography>
  620. </Stack>
  621. )}
  622. {isEditing && editBasic && (
  623. <Stack spacing={1}>
  624. <TextField
  625. size="small"
  626. label={t("Type")}
  627. value={editBasic.description}
  628. onChange={(e) =>
  629. setEditBasic((p) => (p ? { ...p, description: e.target.value } : p))
  630. }
  631. fullWidth
  632. />
  633. <Stack direction="row" spacing={1}>
  634. <TextField
  635. size="small"
  636. label={t("Output Quantity")}
  637. type="number"
  638. value={editBasic.outputQty}
  639. onChange={(e) =>
  640. setEditBasic((p) =>
  641. p ? { ...p, outputQty: Number(e.target.value) } : p
  642. )
  643. }
  644. />
  645. <TextField
  646. size="small"
  647. label={t("Output Quantity UOM")}
  648. value={editBasic.outputQtyUom}
  649. onChange={(e) =>
  650. setEditBasic((p) =>
  651. p ? { ...p, outputQtyUom: e.target.value } : p
  652. )
  653. }
  654. />
  655. </Stack>
  656. <Stack direction="row" spacing={1}>
  657. <TextField
  658. size="small"
  659. label={t("Scrap Rate")}
  660. type="number"
  661. value={editBasic.scrapRate}
  662. onChange={(e) =>
  663. setEditBasic((p) =>
  664. p ? { ...p, scrapRate: Number(e.target.value) } : p
  665. )
  666. }
  667. />
  668. <FormControl size="small" sx={{ minWidth: 160 }}>
  669. <InputLabel>{t("Allergic Substances")}</InputLabel>
  670. <Select
  671. label={t("Allergic Substances")}
  672. value={editBasic.allergicSubstances}
  673. onChange={(e) =>
  674. setEditBasic((p) =>
  675. p
  676. ? {
  677. ...p,
  678. allergicSubstances: Number(e.target.value),
  679. }
  680. : p,
  681. )
  682. }
  683. >
  684. <MenuItem value={0}>有</MenuItem>
  685. <MenuItem value={5}>沒有</MenuItem>
  686. </Select>
  687. </FormControl>
  688. </Stack>
  689. <Stack direction="row" spacing={1}>
  690. <TextField
  691. size="small"
  692. label={t("Depth")}
  693. type="number"
  694. value={editBasic.isDark}
  695. onChange={(e) =>
  696. setEditBasic((p) =>
  697. p ? { ...p, isDark: Number(e.target.value) } : p
  698. )
  699. }
  700. inputProps={{ min: 1, max: 5, step: 1 }}
  701. />
  702. <FormControl size="small" sx={{ minWidth: 140 }}>
  703. <InputLabel>{t("Float")}</InputLabel>
  704. <Select
  705. label={t("Float")}
  706. value={editBasic.isFloat}
  707. onChange={(e) =>
  708. setEditBasic((p) =>
  709. p ? { ...p, isFloat: Number(e.target.value) } : p,
  710. )
  711. }
  712. >
  713. <MenuItem value={5}>沉</MenuItem>
  714. <MenuItem value={3}>浮</MenuItem>
  715. <MenuItem value={0}>不適用</MenuItem>
  716. </Select>
  717. </FormControl>
  718. <FormControl size="small" sx={{ minWidth: 140 }}>
  719. <InputLabel>{t("Density")}</InputLabel>
  720. <Select
  721. label={t("Density")}
  722. value={editBasic.isDense}
  723. onChange={(e) =>
  724. setEditBasic((p) =>
  725. p ? { ...p, isDense: Number(e.target.value) } : p,
  726. )
  727. }
  728. >
  729. <MenuItem value={5}>淡</MenuItem>
  730. <MenuItem value={3}>濃</MenuItem>
  731. <MenuItem value={0}>不適用</MenuItem>
  732. </Select>
  733. </FormControl>
  734. </Stack>
  735. <Stack direction="row" spacing={1}>
  736. <FormControl size="small" sx={{ minWidth: 180 }}>
  737. <InputLabel>{t("Time Sequence")}</InputLabel>
  738. <Select
  739. label={t("Time Sequence")}
  740. value={editBasic.timeSequence}
  741. onChange={(e) =>
  742. setEditBasic((p) =>
  743. p
  744. ? { ...p, timeSequence: Number(e.target.value) }
  745. : p,
  746. )
  747. }
  748. >
  749. <MenuItem value={5}>上午</MenuItem>
  750. <MenuItem value={1}>下午</MenuItem>
  751. <MenuItem value={0}>不適用</MenuItem>
  752. </Select>
  753. </FormControl>
  754. <FormControl size="small" sx={{ minWidth: 180 }}>
  755. <InputLabel>{t("Complexity")}</InputLabel>
  756. <Select
  757. label={t("Complexity")}
  758. value={editBasic.complexity}
  759. onChange={(e) =>
  760. setEditBasic((p) =>
  761. p
  762. ? { ...p, complexity: Number(e.target.value) }
  763. : p,
  764. )
  765. }
  766. >
  767. <MenuItem value={10}>簡單</MenuItem>
  768. <MenuItem value={5}>中度</MenuItem>
  769. <MenuItem value={3}>複雜</MenuItem>
  770. <MenuItem value={0}>不適用</MenuItem>
  771. </Select>
  772. </FormControl>
  773. </Stack>
  774. <FormControlLabel
  775. control={
  776. <Checkbox
  777. checked={editBasic.isDrink}
  778. onChange={(e) =>
  779. setEditBasic((p) =>
  780. p ? { ...p, isDrink: e.target.checked } : p
  781. )
  782. }
  783. />
  784. }
  785. label={t("Is Drink")}
  786. />
  787. </Stack>
  788. )}
  789. </Paper>
  790. {/* 材料列表 */}
  791. <Paper variant="outlined" sx={{ p: 2 }}>
  792. <Typography variant="subtitle1" gutterBottom>
  793. {t("Bom Material")}
  794. </Typography>
  795. <Table size="small">
  796. <TableHead>
  797. <TableRow>
  798. <TableCell> {t("Item Code")}</TableCell>
  799. <TableCell> {t("Item Name")}</TableCell>
  800. <TableCell align="right"> {t("Base Qty")}</TableCell>
  801. <TableCell> {t("Base UOM")}</TableCell>
  802. <TableCell align="right"> {t("Stock Qty")}</TableCell>
  803. <TableCell> {t("Stock UOM")}</TableCell>
  804. <TableCell align="right"> {t("Sales Qty")}</TableCell>
  805. <TableCell> {t("Sales UOM")}</TableCell>
  806. </TableRow>
  807. </TableHead>
  808. <TableBody>
  809. {detail.materials.map((m, i) => (
  810. <TableRow key={i}>
  811. <TableCell>{m.itemCode}</TableCell>
  812. <TableCell>{m.itemName}</TableCell>
  813. <TableCell align="right">{m.baseQty}</TableCell>
  814. <TableCell>{m.baseUom}</TableCell>
  815. <TableCell align="right">{m.stockQty}</TableCell>
  816. <TableCell>{m.stockUom}</TableCell>
  817. <TableCell align="right">{m.salesQty}</TableCell>
  818. <TableCell>{m.salesUom}</TableCell>
  819. </TableRow>
  820. ))}
  821. </TableBody>
  822. </Table>
  823. </Paper>
  824. {/* 製程 + 設備列表 */}
  825. <Paper variant="outlined" sx={{ p: 2 }}>
  826. <Typography variant="subtitle1" gutterBottom>
  827. {t("Process & Equipment")}
  828. </Typography>
  829. {isEditing && (
  830. <Box sx={{ mb: 1 }}>
  831. <Stack direction="row" spacing={1} flexWrap="wrap">
  832. <FormControl size="small" sx={{ minWidth: 200 }}>
  833. <InputLabel>{t("Process Code")}</InputLabel>
  834. <Select
  835. label={t("Process Code")}
  836. value={processAddForm.processCode}
  837. onChange={(e) =>
  838. setProcessAddForm((p) => ({
  839. ...p,
  840. processCode: String(e.target.value),
  841. }))
  842. }
  843. >
  844. <MenuItem value="">
  845. <em>請選擇</em>
  846. </MenuItem>
  847. {processCodeOptions.map((c) => (
  848. <MenuItem key={c} value={c}>
  849. {c}
  850. </MenuItem>
  851. ))}
  852. </Select>
  853. </FormControl>
  854. <FormControl size="small" sx={{ minWidth: 200 }}>
  855. <InputLabel>設備說明</InputLabel>
  856. <Select
  857. label="設備說明"
  858. value={processAddForm.equipmentDescription}
  859. onChange={(e) =>
  860. setProcessAddForm((p) => ({
  861. ...p,
  862. equipmentDescription: String(e.target.value),
  863. }))
  864. }
  865. >
  866. <MenuItem value="">不適用</MenuItem>
  867. {equipmentDescriptionOptions.map((c) => (
  868. <MenuItem key={c} value={c}>
  869. {c}
  870. </MenuItem>
  871. ))}
  872. </Select>
  873. </FormControl>
  874. <FormControl size="small" sx={{ minWidth: 200 }}>
  875. <InputLabel>設備名稱</InputLabel>
  876. <Select
  877. label="設備名稱"
  878. value={processAddForm.equipmentName}
  879. onChange={(e) =>
  880. setProcessAddForm((p) => ({
  881. ...p,
  882. equipmentName: String(e.target.value),
  883. }))
  884. }
  885. >
  886. <MenuItem value="">不適用</MenuItem>
  887. {equipmentNameOptions.map((c) => (
  888. <MenuItem key={c} value={c}>
  889. {c}
  890. </MenuItem>
  891. ))}
  892. </Select>
  893. </FormControl>
  894. <TextField
  895. size="small"
  896. label={t("Process Description")}
  897. value={processAddForm.description}
  898. onChange={(e) =>
  899. setProcessAddForm((p) => ({
  900. ...p,
  901. description: e.target.value,
  902. }))
  903. }
  904. />
  905. <TextField
  906. size="small"
  907. label={t("Duration (Minutes)")}
  908. type="number"
  909. value={processAddForm.durationInMinute}
  910. onChange={(e) =>
  911. setProcessAddForm((p) => ({
  912. ...p,
  913. durationInMinute: Number(e.target.value),
  914. }))
  915. }
  916. />
  917. <TextField
  918. size="small"
  919. label={t("Prep Time (Minutes)")}
  920. type="number"
  921. value={processAddForm.prepTimeInMinute}
  922. onChange={(e) =>
  923. setProcessAddForm((p) => ({
  924. ...p,
  925. prepTimeInMinute: Number(e.target.value),
  926. }))
  927. }
  928. />
  929. <TextField
  930. size="small"
  931. label={t("Post Prod Time (Minutes)")}
  932. type="number"
  933. value={processAddForm.postProdTimeInMinute}
  934. onChange={(e) =>
  935. setProcessAddForm((p) => ({
  936. ...p,
  937. postProdTimeInMinute: Number(e.target.value),
  938. }))
  939. }
  940. />
  941. <Button
  942. size="small"
  943. startIcon={<AddIcon />}
  944. variant="contained"
  945. onClick={addProcessFromForm}
  946. >
  947. {t("Add")}
  948. </Button>
  949. </Stack>
  950. </Box>
  951. )}
  952. <Table size="small">
  953. <TableHead>
  954. <TableRow>
  955. <TableCell> {t("Sequence")}</TableCell>
  956. <TableCell> {t("Process Name")}</TableCell>
  957. <TableCell> {t("Process Description")}</TableCell>
  958. <TableCell> {t("Process Code")}</TableCell>
  959. <TableCell>設備(說明/名稱)</TableCell>
  960. <TableCell align="right"> {t("Duration (Minutes)")}</TableCell>
  961. <TableCell align="right"> {t("Prep Time (Minutes)")}</TableCell>
  962. <TableCell align="right"> {t("Post Prod Time (Minutes)")}</TableCell>
  963. {isEditing && (
  964. <TableCell align="right">{t("Actions")}</TableCell>
  965. )}
  966. </TableRow>
  967. </TableHead>
  968. <TableBody>
  969. {isEditing
  970. ? editProcesses.map((p) => (
  971. <TableRow key={p.key}>
  972. <TableCell>{p.seqNo ?? "-"}</TableCell>
  973. <TableCell>{p.processName || "-"}</TableCell>
  974. <TableCell>
  975. <TextField
  976. size="small"
  977. value={p.description}
  978. onChange={(e) =>
  979. setEditProcesses((prev) =>
  980. prev.map((x) =>
  981. x.key === p.key
  982. ? { ...x, description: e.target.value }
  983. : x,
  984. ),
  985. )
  986. }
  987. />
  988. </TableCell>
  989. <TableCell>
  990. <FormControl size="small" fullWidth>
  991. <Select
  992. value={p.processCode ?? ""}
  993. onChange={(e) =>
  994. setEditProcesses((prev) =>
  995. prev.map((x) =>
  996. x.key === p.key
  997. ? {
  998. ...x,
  999. processCode: String(e.target.value),
  1000. }
  1001. : x,
  1002. ),
  1003. )
  1004. }
  1005. >
  1006. <MenuItem value="">不適用</MenuItem>
  1007. {processCodeOptions.map((c) => (
  1008. <MenuItem key={c} value={c}>
  1009. {c}
  1010. </MenuItem>
  1011. ))}
  1012. </Select>
  1013. </FormControl>
  1014. </TableCell>
  1015. <TableCell>
  1016. <Stack direction="row" spacing={0.5} flexWrap="wrap">
  1017. <FormControl size="small" sx={{ minWidth: 140 }}>
  1018. <Select
  1019. displayEmpty
  1020. value={p.equipmentDescription}
  1021. onChange={(e) =>
  1022. setEditProcesses((prev) =>
  1023. prev.map((x) =>
  1024. x.key === p.key
  1025. ? {
  1026. ...x,
  1027. equipmentDescription: String(
  1028. e.target.value,
  1029. ),
  1030. }
  1031. : x,
  1032. ),
  1033. )
  1034. }
  1035. >
  1036. <MenuItem value="">不適用</MenuItem>
  1037. {equipmentDescriptionOptions.map((c) => (
  1038. <MenuItem key={c} value={c}>
  1039. {c}
  1040. </MenuItem>
  1041. ))}
  1042. </Select>
  1043. </FormControl>
  1044. <FormControl size="small" sx={{ minWidth: 140 }}>
  1045. <Select
  1046. displayEmpty
  1047. value={p.equipmentName}
  1048. onChange={(e) =>
  1049. setEditProcesses((prev) =>
  1050. prev.map((x) =>
  1051. x.key === p.key
  1052. ? {
  1053. ...x,
  1054. equipmentName: String(e.target.value),
  1055. }
  1056. : x,
  1057. ),
  1058. )
  1059. }
  1060. >
  1061. <MenuItem value="">不適用</MenuItem>
  1062. {equipmentNameOptions.map((c) => (
  1063. <MenuItem key={c} value={c}>
  1064. {c}
  1065. </MenuItem>
  1066. ))}
  1067. </Select>
  1068. </FormControl>
  1069. </Stack>
  1070. </TableCell>
  1071. <TableCell align="right">
  1072. <TextField
  1073. size="small"
  1074. type="number"
  1075. value={p.durationInMinute}
  1076. onChange={(e) =>
  1077. setEditProcesses((prev) =>
  1078. prev.map((x) =>
  1079. x.key === p.key
  1080. ? { ...x, durationInMinute: Number(e.target.value) }
  1081. : x,
  1082. ),
  1083. )
  1084. }
  1085. />
  1086. </TableCell>
  1087. <TableCell align="right">
  1088. <TextField
  1089. size="small"
  1090. type="number"
  1091. value={p.prepTimeInMinute}
  1092. onChange={(e) =>
  1093. setEditProcesses((prev) =>
  1094. prev.map((x) =>
  1095. x.key === p.key
  1096. ? { ...x, prepTimeInMinute: Number(e.target.value) }
  1097. : x,
  1098. ),
  1099. )
  1100. }
  1101. />
  1102. </TableCell>
  1103. <TableCell align="right">
  1104. <TextField
  1105. size="small"
  1106. type="number"
  1107. value={p.postProdTimeInMinute}
  1108. onChange={(e) =>
  1109. setEditProcesses((prev) =>
  1110. prev.map((x) =>
  1111. x.key === p.key
  1112. ? {
  1113. ...x,
  1114. postProdTimeInMinute: Number(e.target.value),
  1115. }
  1116. : x,
  1117. ),
  1118. )
  1119. }
  1120. />
  1121. </TableCell>
  1122. <TableCell align="right">
  1123. <IconButton size="small" onClick={() => deleteProcessRow(p.key)}>
  1124. <DeleteIcon fontSize="small" />
  1125. </IconButton>
  1126. </TableCell>
  1127. </TableRow>
  1128. ))
  1129. : detail.processes.map((p, i) => (
  1130. <TableRow key={i}>
  1131. <TableCell>{p.seqNo}</TableCell>
  1132. <TableCell>{p.processName}</TableCell>
  1133. <TableCell>{p.processDescription}</TableCell>
  1134. <TableCell>{p.processCode ?? "-"}</TableCell>
  1135. <TableCell>{p.equipmentCode ?? p.equipmentName}</TableCell>
  1136. <TableCell align="right">{p.durationInMinute}</TableCell>
  1137. <TableCell align="right">{p.prepTimeInMinute}</TableCell>
  1138. <TableCell align="right">
  1139. {p.postProdTimeInMinute}
  1140. </TableCell>
  1141. </TableRow>
  1142. ))}
  1143. </TableBody>
  1144. </Table>
  1145. </Paper>
  1146. </Stack>
  1147. )}
  1148. </Stack>
  1149. );
  1150. };
  1151. export default ImportBomDetailTab;