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

JoSearch.tsx 28 KiB

2ヶ月前
2ヶ月前
3ヶ月前
5ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  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;