FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

ImportBomDetailTab.tsx 41 KiB

1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
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;