FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

709 líneas
28 KiB

  1. "use client"
  2. import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions";
  3. import React, { useCallback, useEffect, useMemo, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { Criterion } from "../SearchBox";
  6. import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults";
  7. import { EditNote } from "@mui/icons-material";
  8. import { arrayToDateString, arrayToDateTimeString, integerFormatter, dayjsToDateString } from "@/app/utils/formatUtil";
  9. import { orderBy, uniqBy, upperFirst } from "lodash";
  10. import SearchBox from "../SearchBox/SearchBox";
  11. import { useRouter } from "next/navigation";
  12. import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
  13. import { StockInLineInput } from "@/app/api/stockIn";
  14. import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo";
  15. import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box } from "@mui/material";
  16. import { BomCombo } from "@/app/api/bom";
  17. import JoCreateFormModal from "./JoCreateFormModal";
  18. import AddIcon from '@mui/icons-material/Add';
  19. import EditIcon from '@mui/icons-material/Edit';
  20. import QcStockInModal from "../Qc/QcStockInModal";
  21. import { useSession } from "next-auth/react";
  22. import { SessionWithTokens } from "@/config/authConfig";
  23. import { createStockInLine } from "@/app/api/stockIn/actions";
  24. import { msg } from "../Swal/CustomAlerts";
  25. import dayjs from "dayjs";
  26. //import { fetchInventories } from "@/app/api/inventory/actions";
  27. import { InventoryResult } from "@/app/api/inventory";
  28. import { PrinterCombo } from "@/app/api/settings/printer";
  29. import { JobTypeResponse } from "@/app/api/jo/actions";
  30. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  31. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  32. import { updateJoPlanStart } from "@/app/api/jo/actions";
  33. import { arrayToDayjs } from "@/app/utils/formatUtil";
  34. interface Props {
  35. defaultInputs: SearchJoResultRequest,
  36. bomCombo: BomCombo[]
  37. printerCombo: PrinterCombo[];
  38. jobTypes: JobTypeResponse[];
  39. }
  40. type SearchQuery = Partial<Omit<JobOrder, "id">>;
  41. type SearchParamNames = keyof SearchQuery;
  42. const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
  43. const { t } = useTranslation("jo");
  44. const router = useRouter()
  45. const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
  46. const [inputs, setInputs] = useState(defaultInputs);
  47. const [pagingController, setPagingController] = useState(
  48. defaultPagingController
  49. )
  50. const [totalCount, setTotalCount] = useState(0)
  51. const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)
  52. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  53. const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
  54. // 合并后的统一编辑 Dialog 状态
  55. const [openEditDialog, setOpenEditDialog] = useState(false);
  56. const [selectedJoForEdit, setSelectedJoForEdit] = useState<JobOrder | null>(null);
  57. const [editPlanStartDate, setEditPlanStartDate] = useState<dayjs.Dayjs | null>(null);
  58. const [editReqQtyMultiplier, setEditReqQtyMultiplier] = useState<number>(1);
  59. const [editBomForReqQty, setEditBomForReqQty] = useState<BomCombo | null>(null);
  60. const [editProductionPriority, setEditProductionPriority] = useState<number>(50);
  61. const [editProductProcessId, setEditProductProcessId] = useState<number | null>(null);
  62. const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
  63. const response = await fetch(`/api/jo/detail?id=${id}`);
  64. if (!response.ok) {
  65. throw new Error('Failed to fetch JO detail');
  66. }
  67. return response.json();
  68. };
  69. useEffect(() => {
  70. const fetchDetailedJos = async () => {
  71. const detailedMap = new Map<number, JobOrder>();
  72. try {
  73. const results = await Promise.all(
  74. filteredJos.map((jo) =>
  75. fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => {
  76. console.error(`Error fetching detail for JO ${jo.id}:`, error);
  77. return null;
  78. })
  79. )
  80. );
  81. results.forEach((r) => {
  82. if (r) detailedMap.set(r.id, r.detail);
  83. });
  84. } catch (error) {
  85. console.error("Error fetching JO details:", error);
  86. }
  87. setDetailedJos(detailedMap);
  88. };
  89. if (filteredJos.length > 0) {
  90. fetchDetailedJos();
  91. }
  92. }, [filteredJos]);
  93. /*
  94. useEffect(() => {
  95. const fetchInventoryData = async () => {
  96. try {
  97. const inventoryResponse = await fetchInventories({
  98. code: "",
  99. name: "",
  100. type: "",
  101. pageNum: 0,
  102. pageSize: 200,
  103. });
  104. setInventoryData(inventoryResponse.records ?? []);
  105. } catch (error) {
  106. console.error("Error fetching inventory data:", error);
  107. }
  108. };
  109. fetchInventoryData();
  110. }, []);
  111. */
  112. const getStockAvailable = (pickLine: JoDetailPickLine) => {
  113. const inventory = inventoryData.find(inventory =>
  114. inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
  115. );
  116. if (inventory) {
  117. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  118. }
  119. return 0;
  120. };
  121. const isStockSufficient = (pickLine: JoDetailPickLine) => {
  122. const stockAvailable = getStockAvailable(pickLine);
  123. return stockAvailable >= pickLine.reqQty;
  124. };
  125. const getStockCounts = (jo: JobOrder) => {
  126. return {
  127. sufficient: jo.sufficientCount,
  128. insufficient: jo.insufficientCount
  129. };
  130. };
  131. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
  132. { label: t("Code"), paramName: "code", type: "text" },
  133. { label: t("Item Name"), paramName: "itemName", type: "text" },
  134. { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: {
  135. from: dayjsToDateString(dayjs(), "input"),
  136. to: dayjsToDateString(dayjs(), "input")
  137. } },
  138. {
  139. label: t("Job Type"),
  140. paramName: "jobTypeName",
  141. type: "select",
  142. options: jobTypes.map(jt => jt.name)
  143. },
  144. ], [t, jobTypes])
  145. const fetchBomForJo = useCallback(async (jo: JobOrder): Promise<BomCombo | null> => {
  146. try {
  147. const detailedJo = detailedJos.get(jo.id) || await fetchJoDetailClient(jo.id);
  148. const matchingBom = bomCombo.find(bom => {
  149. return true; // 临时占位
  150. });
  151. return matchingBom || null;
  152. } catch (error) {
  153. console.error("Error fetching BOM for JO:", error);
  154. return null;
  155. }
  156. }, [bomCombo, detailedJos]);
  157. // 统一的打开编辑对话框函数
  158. const handleOpenEditDialog = useCallback(async (jo: JobOrder) => {
  159. setSelectedJoForEdit(jo);
  160. // 设置 Plan Start Date
  161. if (jo.planStart && Array.isArray(jo.planStart)) {
  162. setEditPlanStartDate(arrayToDayjs(jo.planStart));
  163. } else {
  164. setEditPlanStartDate(dayjs());
  165. }
  166. // 设置 Production Priority
  167. setEditProductionPriority(jo.productionPriority ?? 50);
  168. // 获取 productProcessId
  169. try {
  170. const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions");
  171. const processes = await fetchProductProcessesByJobOrderId(jo.id);
  172. if (processes && processes.length > 0) {
  173. setEditProductProcessId(processes[0].id);
  174. }
  175. } catch (error) {
  176. console.error("Error fetching product process:", error);
  177. }
  178. // 设置 ReqQty
  179. const bom = await fetchBomForJo(jo);
  180. if (bom) {
  181. setEditBomForReqQty(bom);
  182. const currentMultiplier = bom.outputQty > 0
  183. ? Math.round(jo.reqQty / bom.outputQty)
  184. : 1;
  185. setEditReqQtyMultiplier(currentMultiplier);
  186. }
  187. setOpenEditDialog(true);
  188. }, [fetchBomForJo]);
  189. // 统一的关闭函数
  190. const handleCloseEditDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  191. setOpenEditDialog(false);
  192. setSelectedJoForEdit(null);
  193. setEditPlanStartDate(null);
  194. setEditReqQtyMultiplier(1);
  195. setEditBomForReqQty(null);
  196. setEditProductionPriority(50);
  197. setEditProductProcessId(null);
  198. }, []);
  199. const columns = useMemo<Column<JobOrder>[]>(
  200. () => [
  201. {
  202. name: "planStart",
  203. label: t("Estimated Production Date"),
  204. align: "left",
  205. headerAlign: "left",
  206. renderCell: (row) => {
  207. return (
  208. <Stack direction="row" alignItems="center" spacing={1}>
  209. {row.status == "planning" && (
  210. <IconButton
  211. size="small"
  212. onClick={(e) => {
  213. e.stopPropagation();
  214. handleOpenEditDialog(row);
  215. }}
  216. sx={{ padding: '4px' }}
  217. >
  218. <EditIcon fontSize="small" />
  219. </IconButton>
  220. )}
  221. <span>{row.planStart ? arrayToDateString(row.planStart) : '-'}</span>
  222. </Stack>
  223. );
  224. }
  225. },
  226. {
  227. name: "productionPriority",
  228. label: t("Production Priority"),
  229. renderCell: (row) => {
  230. return (
  231. <Stack direction="row" alignItems="center" spacing={1}>
  232. <span>{integerFormatter.format(row.productionPriority)}</span>
  233. </Stack>
  234. );
  235. }
  236. },
  237. {
  238. name: "code",
  239. label: t("Code"),
  240. flex: 2
  241. },
  242. {
  243. name: "item",
  244. label: `${t("Item Name")}`,
  245. renderCell: (row) => {
  246. return row.item ? <>{t(row.jobTypeName)} {t(row.item.code)} {t(row.item.name)}</> : '-'
  247. }
  248. },
  249. {
  250. name: "reqQty",
  251. label: t("Req. Qty"),
  252. align: "right",
  253. headerAlign: "right",
  254. renderCell: (row) => {
  255. return (
  256. <Stack direction="row" alignItems="center" spacing={1} justifyContent="flex-end">
  257. <span>{integerFormatter.format(row.reqQty)}</span>
  258. </Stack>
  259. );
  260. }
  261. },
  262. {
  263. name: "item",
  264. label: t("UoM"),
  265. align: "left",
  266. headerAlign: "left",
  267. renderCell: (row) => {
  268. return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
  269. }
  270. },
  271. {
  272. name: "stockStatus" as keyof JobOrder,
  273. label: t("BOM Status"),
  274. align: "left",
  275. headerAlign: "left",
  276. renderCell: (row) => {
  277. const stockCounts = getStockCounts(row);
  278. return (
  279. <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
  280. {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
  281. </span>
  282. );
  283. }
  284. },
  285. {
  286. name: "status",
  287. label: t("Status"),
  288. renderCell: (row) => {
  289. return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
  290. {t(upperFirst(row.status))}
  291. </span>
  292. }
  293. },
  294. {
  295. name: "id",
  296. label: t("Actions"),
  297. renderCell: (row) => {
  298. return (
  299. <Button
  300. id="emailSupplier"
  301. type="button"
  302. variant="contained"
  303. color="primary"
  304. sx={{ width: "150px" }}
  305. onClick={() => onDetailClick(row)}
  306. >
  307. {t("View")}
  308. </Button>
  309. )
  310. }
  311. },
  312. ], [t, inventoryData, detailedJos, handleOpenEditDialog]
  313. )
  314. const newPageFetch = useCallback(
  315. async (
  316. pagingController: { pageNum: number; pageSize: number },
  317. filterArgs: SearchJoResultRequest,
  318. ) => {
  319. const params: SearchJoResultRequest = {
  320. ...filterArgs,
  321. pageNum: pagingController.pageNum - 1,
  322. pageSize: pagingController.pageSize,
  323. };
  324. const response = await fetchJos(params);
  325. console.log("newPageFetch params:", params)
  326. console.log("newPageFetch response:", response)
  327. if (response && response.records) {
  328. console.log("newPageFetch - setting filteredJos with", response.records.length, "records");
  329. setTotalCount(response.total);
  330. setFilteredJos(response.records);
  331. console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id);
  332. } else {
  333. console.warn("newPageFetch - no response or no records");
  334. setFilteredJos([]);
  335. }
  336. },
  337. [],
  338. );
  339. const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
  340. try {
  341. const response = await updateJoReqQty({
  342. id: jobOrderId,
  343. reqQty: newReqQty
  344. });
  345. if (response) {
  346. msg(t("update success"));
  347. await newPageFetch(pagingController, inputs);
  348. }
  349. } catch (error) {
  350. console.error("Error updating reqQty:", error);
  351. msg(t("update failed"));
  352. }
  353. }, [pagingController, inputs, newPageFetch, t]);
  354. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  355. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  356. if (response) {
  357. await newPageFetch(pagingController, inputs);
  358. }
  359. }, [pagingController, inputs, newPageFetch]);
  360. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  361. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  362. if (response) {
  363. await newPageFetch(pagingController, inputs);
  364. }
  365. }, [pagingController, inputs, newPageFetch]);
  366. // 统一的确认函数
  367. const handleConfirmEdit = useCallback(async () => {
  368. if (!selectedJoForEdit) return;
  369. try {
  370. // 更新 Plan Start
  371. if (editPlanStartDate) {
  372. const dateString = `${dayjsToDateString(editPlanStartDate, "input")}T00:00:00`;
  373. await handleUpdatePlanStart(selectedJoForEdit.id, dateString);
  374. }
  375. // 更新 ReqQty
  376. if (editBomForReqQty) {
  377. const newReqQty = editReqQtyMultiplier * editBomForReqQty.outputQty;
  378. await handleUpdateReqQty(selectedJoForEdit.id, newReqQty);
  379. }
  380. // 更新 Production Priority
  381. if (editProductProcessId) {
  382. await handleUpdateOperationPriority(editProductProcessId, Number(editProductionPriority));
  383. }
  384. setOpenEditDialog(false);
  385. setSelectedJoForEdit(null);
  386. setEditPlanStartDate(null);
  387. setEditReqQtyMultiplier(1);
  388. setEditBomForReqQty(null);
  389. setEditProductionPriority(50);
  390. setEditProductProcessId(null);
  391. } catch (error) {
  392. console.error("Error updating:", error);
  393. msg(t("update failed"));
  394. }
  395. }, [selectedJoForEdit, editPlanStartDate, editBomForReqQty, editReqQtyMultiplier, editProductionPriority, editProductProcessId, handleUpdatePlanStart, handleUpdateReqQty, handleUpdateOperationPriority, t]);
  396. useEffect(() => {
  397. newPageFetch(pagingController, inputs);
  398. }, [newPageFetch, pagingController, inputs]);
  399. const handleUpdate = useCallback(async (jo: JobOrder) => {
  400. console.log(jo);
  401. try {
  402. if (jo.id) {
  403. const response = await updateJo({ id: jo.id, status: "storing" });
  404. console.log(`%c Updated JO:`, "color:lime", response);
  405. const postData = {
  406. itemId: jo?.item?.id!!,
  407. acceptedQty: jo?.reqQty ?? 1,
  408. productLotNo: jo?.code,
  409. productionDate: arrayToDateString(dayjs(), "input"),
  410. jobOrderId: jo?.id,
  411. };
  412. const res = await createStockInLine(postData);
  413. console.log(`%c Created Stock In Line`, "color:lime", res);
  414. msg(t("update success"));
  415. setInputs(defaultInputs);
  416. setPagingController(defaultPagingController);
  417. }
  418. } catch (e) {
  419. console.log(e);
  420. } finally {
  421. // setIsUploading(false)
  422. }
  423. }, [defaultInputs, t])
  424. const getButtonSx = (jo : JobOrder) => {
  425. const joStatus = jo.status?.toLowerCase();
  426. const silStatus = jo.stockInLineStatus?.toLowerCase();
  427. let btnSx = {label:"", color:""};
  428. switch (joStatus) {
  429. case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
  430. case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
  431. case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
  432. case "storing":
  433. switch (silStatus) {
  434. case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break;
  435. case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
  436. case "escalated":
  437. if (sessionToken?.id == jo.silHandlerId) {
  438. btnSx = {label: t("escalation processing"), color:"warning.main"};
  439. break;
  440. }
  441. default: btnSx = {label: t("view stockin"), color:"info.main"};
  442. }
  443. break;
  444. case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
  445. default: btnSx = {label: t("scan picked material"), color:"success.main"};
  446. }
  447. return btnSx
  448. };
  449. const { data: session } = useSession();
  450. const sessionToken = session as SessionWithTokens | null;
  451. const [openModal, setOpenModal] = useState<boolean>(false);
  452. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  453. const onDetailClick = useCallback((record: JobOrder) => {
  454. router.push(`/jo/edit?id=${record.id}`)
  455. }, [router])
  456. const closeNewModal = useCallback(() => {
  457. setOpenModal(false);
  458. setInputs(defaultInputs);
  459. setPagingController(defaultPagingController);
  460. }, [defaultInputs]);
  461. const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
  462. const transformedQuery = {
  463. ...query,
  464. planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart,
  465. planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo,
  466. jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : ""
  467. };
  468. setInputs({
  469. code: transformedQuery.code,
  470. itemName: transformedQuery.itemName,
  471. planStart: transformedQuery.planStart,
  472. planStartTo: transformedQuery.planStartTo,
  473. jobTypeName: transformedQuery.jobTypeName
  474. });
  475. setPagingController(defaultPagingController);
  476. }, [defaultInputs])
  477. const onReset = useCallback(() => {
  478. setInputs(defaultInputs);
  479. setPagingController(defaultPagingController);
  480. }, [defaultInputs])
  481. const onOpenCreateJoModal = useCallback(() => {
  482. setIsCreateJoModalOpen(() => true)
  483. }, [])
  484. const onCloseCreateJoModal = useCallback(() => {
  485. setIsCreateJoModalOpen(() => false)
  486. }, [])
  487. return <>
  488. <Stack
  489. direction="row"
  490. justifyContent="flex-end"
  491. spacing={2}
  492. sx={{ mt: 2 }}
  493. >
  494. <Button
  495. variant="outlined"
  496. startIcon={<AddIcon />}
  497. onClick={onOpenCreateJoModal}
  498. >
  499. {t("Create Job Order")}
  500. </Button>
  501. </Stack>
  502. <SearchBox
  503. criteria={searchCriteria}
  504. onSearch={onSearch}
  505. onReset={onReset}
  506. />
  507. <SearchResults<JobOrder>
  508. items={filteredJos}
  509. columns={columns}
  510. setPagingController={setPagingController}
  511. pagingController={pagingController}
  512. totalCount={totalCount}
  513. isAutoPaging={false}
  514. />
  515. <JoCreateFormModal
  516. open={isCreateJoModalOpen}
  517. bomCombo={bomCombo}
  518. jobTypes={jobTypes}
  519. onClose={onCloseCreateJoModal}
  520. onSearch={() => {
  521. setInputs({ ...defaultInputs });
  522. setPagingController(defaultPagingController);
  523. }}
  524. />
  525. <QcStockInModal
  526. session={sessionToken}
  527. open={openModal}
  528. onClose={closeNewModal}
  529. inputDetail={modalInfo}
  530. printerCombo={printerCombo}
  531. />
  532. {/* 合并后的统一编辑 Dialog */}
  533. <Dialog
  534. open={openEditDialog}
  535. onClose={handleCloseEditDialog}
  536. fullWidth
  537. maxWidth="sm"
  538. >
  539. <DialogTitle>{t("Edit Job Order")}</DialogTitle>
  540. <DialogContent>
  541. <Stack spacing={3} sx={{ mt: 1 }}>
  542. {/* Plan Start Date */}
  543. <LocalizationProvider dateAdapter={AdapterDayjs}>
  544. <DatePicker
  545. label={t("Estimated Production Date")}
  546. value={editPlanStartDate}
  547. onChange={(newValue) => setEditPlanStartDate(newValue)}
  548. slotProps={{
  549. textField: {
  550. fullWidth: true,
  551. margin: "dense",
  552. }
  553. }}
  554. />
  555. </LocalizationProvider>
  556. {/* Production Priority */}
  557. <TextField
  558. label={t("Production Priority")}
  559. type="number"
  560. fullWidth
  561. value={editProductionPriority}
  562. onChange={(e) => {
  563. const val = Number(e.target.value);
  564. if (val >= 1 && val <= 100) {
  565. setEditProductionPriority(val);
  566. }
  567. }}
  568. inputProps={{
  569. min: 1,
  570. max: 100,
  571. step: 1
  572. }}
  573. />
  574. {/* ReqQty */}
  575. {editBomForReqQty && (
  576. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  577. <TextField
  578. label={t("Base Qty")}
  579. fullWidth
  580. type="number"
  581. variant="outlined"
  582. value={editBomForReqQty.outputQty}
  583. disabled
  584. InputProps={{
  585. endAdornment: editBomForReqQty.outputQtyUom ? (
  586. <InputAdornment position="end">
  587. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  588. {editBomForReqQty.outputQtyUom}
  589. </Typography>
  590. </InputAdornment>
  591. ) : null
  592. }}
  593. sx={{ flex: 1 }}
  594. />
  595. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  596. ×
  597. </Typography>
  598. <TextField
  599. label={t("Batch Count")}
  600. fullWidth
  601. type="number"
  602. variant="outlined"
  603. value={editReqQtyMultiplier}
  604. onChange={(e) => {
  605. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  606. setEditReqQtyMultiplier(val);
  607. }}
  608. inputProps={{
  609. min: 1,
  610. step: 1
  611. }}
  612. sx={{ flex: 1 }}
  613. />
  614. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  615. =
  616. </Typography>
  617. <TextField
  618. label={t("Req. Qty")}
  619. fullWidth
  620. variant="outlined"
  621. type="number"
  622. value={editBomForReqQty ? (editReqQtyMultiplier * editBomForReqQty.outputQty) : ""}
  623. disabled
  624. InputProps={{
  625. endAdornment: editBomForReqQty.outputQtyUom ? (
  626. <InputAdornment position="end">
  627. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  628. {editBomForReqQty.outputQtyUom}
  629. </Typography>
  630. </InputAdornment>
  631. ) : null
  632. }}
  633. sx={{ flex: 1 }}
  634. />
  635. </Box>
  636. )}
  637. </Stack>
  638. </DialogContent>
  639. <DialogActions>
  640. <Button onClick={handleCloseEditDialog}>{t("Cancel")}</Button>
  641. <Button
  642. variant="contained"
  643. onClick={handleConfirmEdit}
  644. disabled={!editPlanStartDate || !editBomForReqQty}
  645. >
  646. {t("Save")}
  647. </Button>
  648. </DialogActions>
  649. </Dialog>
  650. </>
  651. }
  652. export default JoSearch;