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

ProductionProcessJobOrderDetail.tsx 30 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3週間前
2ヶ月前
4週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
3週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3週間前
2ヶ月前
1ヶ月前
4週間前
3週間前
2ヶ月前
3週間前
4週間前
2ヶ月前
2ヶ月前
3週間前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4週間前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3日前
2ヶ月前
3日前
2ヶ月前
3日前
2ヶ月前
3日前
3週間前
2ヶ月前
2週間前
2ヶ月前
3日前
3週間前
3日前
3週間前
3日前
3週間前
3日前
3週間前
3日前
3週間前
3日前
3週間前
2ヶ月前
3日前
3週間前
3日前
3週間前
3日前
3週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3日前
2ヶ月前
2ヶ月前
3日前
3週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
4週間前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. "use client";
  2. import React, { useCallback, useEffect, useState, useMemo } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Paper,
  7. Stack,
  8. Typography,
  9. TextField,
  10. Grid,
  11. Card,
  12. CardContent,
  13. CircularProgress,
  14. Tabs,
  15. Tab,
  16. TabsProps,
  17. IconButton,
  18. Dialog,
  19. DialogTitle,
  20. DialogContent,
  21. DialogActions,
  22. InputAdornment
  23. } from "@mui/material";
  24. import ArrowBackIcon from '@mui/icons-material/ArrowBack';
  25. import { useTranslation } from "react-i18next";
  26. import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions";
  27. import ProductionProcessDetail from "./ProductionProcessDetail";
  28. import { BomCombo } from "@/app/api/bom";
  29. import { fetchBomCombo } from "@/app/api/bom/index";
  30. import dayjs from "dayjs";
  31. import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
  32. import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
  33. import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
  34. import { decimalFormatter } from "@/app/utils/formatUtil";
  35. import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
  36. import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
  37. import { fetchInventories } from "@/app/api/inventory/actions";
  38. import { InventoryResult } from "@/app/api/inventory";
  39. import { releaseJo, startJo } from "@/app/api/jo/actions";
  40. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  41. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  42. import EditIcon from "@mui/icons-material/Edit";
  43. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  44. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  45. import { dayjsToDateString } from "@/app/utils/formatUtil";
  46. interface ProductProcessJobOrderDetailProps {
  47. jobOrderId: number;
  48. onBack: () => void;
  49. fromJosave?: boolean;
  50. }
  51. const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
  52. jobOrderId,
  53. onBack,
  54. fromJosave,
  55. }) => {
  56. const { t } = useTranslation();
  57. const [loading, setLoading] = useState(false);
  58. const [processData, setProcessData] = useState<any>(null);
  59. const [jobOrderLines, setJobOrderLines] = useState<JobOrderLineInfo[]>([]);
  60. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  61. const [tabIndex, setTabIndex] = useState(0);
  62. const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
  63. const [operationPriority, setOperationPriority] = useState<number>(50);
  64. const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false);
  65. const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false);
  66. const [planStartDate, setPlanStartDate] = useState<dayjs.Dayjs | null>(null);
  67. const [openReqQtyDialog, setOpenReqQtyDialog] = useState(false);
  68. const [reqQtyMultiplier, setReqQtyMultiplier] = useState<number>(1);
  69. const [selectedBomForReqQty, setSelectedBomForReqQty] = useState<BomCombo | null>(null);
  70. const [bomCombo, setBomCombo] = useState<BomCombo[]>([]);
  71. const [showBaseQty, setShowBaseQty] = useState<boolean>(false);
  72. const fetchData = useCallback(async () => {
  73. setLoading(true);
  74. try {
  75. const data = await fetchProductProcessesByJobOrderId(jobOrderId);
  76. if (data && data.length > 0) {
  77. const firstProcess = data[0];
  78. setProcessData(firstProcess);
  79. setJobOrderLines((firstProcess as any).jobOrderLines || []);
  80. }
  81. } catch (error) {
  82. console.error("Error loading data:", error);
  83. } finally {
  84. setLoading(false);
  85. }
  86. }, [jobOrderId]);
  87. const toggleBaseQty = useCallback(() => {
  88. setShowBaseQty(prev => !prev);
  89. }, []);
  90. // 4. 添加处理函数(约第 166 行后)
  91. const handleOpenReqQtyDialog = useCallback(async () => {
  92. if (!processData || !processData.outputQty || !processData.outputQtyUom) {
  93. alert(t("BOM data not available"));
  94. return;
  95. }
  96. const baseOutputQty = processData.bomBaseQty;
  97. const currentMultiplier = baseOutputQty > 0
  98. ? Math.round(processData.outputQty / baseOutputQty)
  99. : 1;
  100. const bomData = {
  101. id: processData.bomId || 0,
  102. value: processData.bomId || 0,
  103. label: processData.bomDescription || "",
  104. outputQty: baseOutputQty,
  105. outputQtyUom: processData.outputQtyUom,
  106. description: processData.bomDescription || ""
  107. };
  108. setSelectedBomForReqQty(bomData);
  109. setReqQtyMultiplier(currentMultiplier);
  110. setOpenReqQtyDialog(true);
  111. }, [processData, t]);
  112. const handleCloseReqQtyDialog = useCallback(() => {
  113. setOpenReqQtyDialog(false);
  114. setSelectedBomForReqQty(null);
  115. setReqQtyMultiplier(1);
  116. }, []);
  117. const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
  118. try {
  119. const response = await updateJoReqQty({
  120. id: jobOrderId,
  121. reqQty: Math.round(newReqQty)
  122. });
  123. if (response) {
  124. await fetchData();
  125. }
  126. } catch (error) {
  127. console.error("Error updating reqQty:", error);
  128. alert(t("update failed"));
  129. }
  130. }, [fetchData, t]);
  131. const handleConfirmReqQty = useCallback(async () => {
  132. if (!jobOrderId || !selectedBomForReqQty) return;
  133. const newReqQty = reqQtyMultiplier * selectedBomForReqQty.outputQty;
  134. await handleUpdateReqQty(jobOrderId, newReqQty);
  135. setOpenReqQtyDialog(false);
  136. setSelectedBomForReqQty(null);
  137. setReqQtyMultiplier(1);
  138. }, [jobOrderId, selectedBomForReqQty, reqQtyMultiplier, handleUpdateReqQty]);
  139. // 获取库存数据
  140. useEffect(() => {
  141. const fetchInventoryData = async () => {
  142. try {
  143. const inventoryResponse = await fetchInventories({
  144. code: "",
  145. name: "",
  146. type: "",
  147. pageNum: 0,
  148. pageSize: 1000
  149. });
  150. setInventoryData(inventoryResponse.records);
  151. } catch (error) {
  152. console.error("Error fetching inventory data:", error);
  153. }
  154. };
  155. fetchInventoryData();
  156. }, []);
  157. useEffect(() => {
  158. fetchData();
  159. }, [fetchData]);
  160. // PickTable 组件内容
  161. const getStockAvailable = (line: JobOrderLineInfo) => {
  162. if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") {
  163. return line.stockQty || 0;
  164. }
  165. const inventory = inventoryData.find(inv =>
  166. inv.itemCode === line.itemCode || inv.itemName === line.itemName
  167. );
  168. if (inventory) {
  169. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  170. }
  171. return line.stockQty || 0;
  172. };
  173. const handleOpenPlanStartDialog = useCallback(() => {
  174. // 将 processData.date 转换为 dayjs 对象
  175. if (processData?.date) {
  176. // processData.date 可能是字符串或 Date 对象
  177. setPlanStartDate(dayjs(processData.date));
  178. } else {
  179. setPlanStartDate(dayjs());
  180. }
  181. setOpenPlanStartDialog(true);
  182. }, [processData?.date]);
  183. const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  184. setOpenPlanStartDialog(false);
  185. setPlanStartDate(null);
  186. }, []);
  187. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  188. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  189. if (response) {
  190. await fetchData();
  191. }
  192. }, [fetchData]);
  193. const handleConfirmPlanStart = useCallback(async () => {
  194. if (!jobOrderId || !planStartDate) return;
  195. // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
  196. const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
  197. await handleUpdatePlanStart(jobOrderId, dateString);
  198. setOpenPlanStartDialog(false);
  199. setPlanStartDate(null);
  200. }, [jobOrderId, planStartDate, handleUpdatePlanStart]);
  201. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  202. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  203. if (response) {
  204. await fetchData();
  205. }
  206. }, [jobOrderId]);
  207. const handleOpenPriorityDialog = () => {
  208. setOperationPriority(processData?.productionPriority ?? 50);
  209. setOpenOperationPriorityDialog(true);
  210. };
  211. const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  212. setOpenOperationPriorityDialog(false);
  213. };
  214. const handleConfirmPriority = async () => {
  215. if (!processData?.id) return;
  216. await handleUpdateOperationPriority(processData.id, Number(operationPriority));
  217. setOpenOperationPriorityDialog(false);
  218. };
  219. const isStockSufficient = (line: JobOrderLineInfo) => {
  220. if (line.type?.toLowerCase() === "consumables") {
  221. return false;
  222. }
  223. const stockAvailable = getStockAvailable(line);
  224. if (stockAvailable === null) {
  225. return false;
  226. }
  227. return stockAvailable >= line.reqQty;
  228. };
  229. const stockCounts = useMemo(() => {
  230. // 过滤掉 consumables 类型的 lines
  231. const nonConsumablesLines = jobOrderLines.filter(
  232. line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" && line.type?.toLowerCase() !== "nm"
  233. );
  234. const total = nonConsumablesLines.length;
  235. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  236. return {
  237. total,
  238. sufficient,
  239. insufficient: total - sufficient,
  240. };
  241. }, [jobOrderLines, inventoryData]);
  242. const status = processData?.status?.toLowerCase?.() ?? "";
  243. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  244. const response = await deleteJobOrder(jobOrderId)
  245. if (response) {
  246. //setProcessData(response.entity);
  247. //await fetchData();
  248. onBack();
  249. }
  250. }, [jobOrderId]);
  251. const handleRelease = useCallback(async ( jobOrderId: number) => {
  252. // TODO: 替换为实际的 release 调用
  253. console.log("Release clicked for jobOrderId:", jobOrderId);
  254. const response = await releaseJo({ id: jobOrderId })
  255. if (response) {
  256. //setProcessData(response.entity);
  257. await fetchData();
  258. }
  259. }, [jobOrderId]);
  260. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  261. (_e, newValue) => {
  262. setTabIndex(newValue);
  263. },
  264. [],
  265. );
  266. // 如果选择了 process detail,显示 detail 页面
  267. if (selectedProcessId !== null) {
  268. return (
  269. <ProductionProcessDetail
  270. jobOrderId={selectedProcessId}
  271. onBack={() => {
  272. setSelectedProcessId(null);
  273. fetchData(); // 刷新数据
  274. }}
  275. />
  276. );
  277. }
  278. if (loading) {
  279. return (
  280. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  281. <CircularProgress/>
  282. </Box>
  283. );
  284. }
  285. if (!processData) {
  286. return (
  287. <Box>
  288. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  289. {t("Back")}
  290. </Button>
  291. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  292. </Box>
  293. );
  294. }
  295. // InfoCard 组件内容
  296. const InfoCardContent = () => (
  297. <Card sx={{ display: "block", mt: 2 }}>
  298. <CardContent component={Stack} spacing={4}>
  299. <Box>
  300. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  301. <Grid item xs={6}>
  302. <TextField
  303. label={t("Job Order Code")}
  304. fullWidth
  305. disabled={true}
  306. value={processData?.jobOrderCode || ""}
  307. />
  308. </Grid>
  309. <Grid item xs={6}>
  310. <TextField
  311. label={t("Item Code")}
  312. fullWidth
  313. disabled={true}
  314. value={processData?.itemCode+"-"+processData?.itemName || ""}
  315. />
  316. </Grid>
  317. <Grid item xs={6}>
  318. <TextField
  319. label={t("Job Type")}
  320. fullWidth
  321. disabled={true}
  322. value={t(processData?.jobType) || t("N/A")}
  323. //value={t("N/A")}
  324. />
  325. </Grid>
  326. <Grid item xs={6}>
  327. <TextField
  328. label={t("Req. Qty")}
  329. fullWidth
  330. disabled={true}
  331. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  332. InputProps={{
  333. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  334. <InputAdornment position="end">
  335. <IconButton size="small" onClick={handleOpenReqQtyDialog}>
  336. <EditIcon fontSize="small" />
  337. </IconButton>
  338. </InputAdornment>
  339. ) : null),
  340. }}
  341. />
  342. </Grid>
  343. <Grid item xs={6}>
  344. <TextField
  345. value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}
  346. label={t("Target Production Date")}
  347. fullWidth
  348. disabled={true}
  349. InputProps={{
  350. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  351. <InputAdornment position="end">
  352. <IconButton size="small" onClick={handleOpenPlanStartDialog}>
  353. <EditIcon fontSize="small" />
  354. </IconButton>
  355. </InputAdornment>
  356. ) : null),
  357. }}
  358. />
  359. </Grid>
  360. <Grid item xs={6}>
  361. <TextField
  362. label={t("Production Priority")}
  363. fullWidth
  364. disabled={true}
  365. value={processData?.productionPriority ?? "50"}
  366. InputProps={{
  367. endAdornment: (
  368. <InputAdornment position="end">
  369. <IconButton size="small" onClick={handleOpenPriorityDialog}>
  370. <EditIcon fontSize="small" />
  371. </IconButton>
  372. </InputAdornment>
  373. ),
  374. }}
  375. />
  376. </Grid>
  377. <Grid item xs={6}>
  378. <TextField
  379. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity")}
  380. fullWidth
  381. disabled={true}
  382. value={`${processData?.isDark == null || processData?.isDark === "" ? t("N/A") : processData.isDark} | ${processData?.isDense == null || processData?.isDense === "" || processData?.isDense === 0 ? t("N/A") : processData.isDense} | ${processData?.isFloat == null || processData?.isFloat === "" ? t("N/A") : processData.isFloat} | ${processData?.scrapRate == -1 || processData?.scrapRate === "" ? t("N/A") : processData.scrapRate} | ${processData?.allergicSubstance == null || processData?.allergicSubstance === "" ? t("N/A") :t (processData.allergicSubstance)} | ${processData?.timeSequence == null || processData?.timeSequence === "" ? t("N/A") : processData.timeSequence} | ${processData?.complexity == null || processData?.complexity === "" ? t("N/A") : processData.complexity}`}
  383. />
  384. </Grid>
  385. </Grid>
  386. </Box>
  387. </CardContent>
  388. </Card>
  389. );
  390. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  391. {
  392. field: "seqNo",
  393. headerName: t("Seq"),
  394. flex: 0.2,
  395. align: "left",
  396. headerAlign: "left",
  397. type: "number",
  398. renderCell: (params) => {
  399. return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
  400. },
  401. },
  402. {
  403. field: "description",
  404. headerName: t("Remark"),
  405. flex: 1,
  406. align: "left",
  407. headerAlign: "left",
  408. renderCell: (params) => {
  409. return(
  410. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  411. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  412. <Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
  413. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  414. </Box>
  415. )
  416. },
  417. },
  418. ];
  419. const productionProcessesLineRemarkTableRows =
  420. processData?.productProcessLines?.map((line: any) => ({
  421. id: line.seqNo,
  422. seqNo: line.seqNo,
  423. description: line.description ?? "",
  424. })) ?? [];
  425. const pickTableColumns: GridColDef[] = [
  426. {
  427. field: "id",
  428. headerName: t("id"),
  429. flex: 0.2,
  430. align: "left",
  431. headerAlign: "left",
  432. type: "number",
  433. sortable: false, // ✅ 禁用排序
  434. },
  435. {
  436. field: "itemCode",
  437. headerName: t("Material Code"),
  438. flex: 0.6,
  439. sortable: false, // ✅ 禁用排序
  440. },
  441. {
  442. field: "itemName",
  443. headerName: t("Item Name"),
  444. flex: 1,
  445. sortable: false, // ✅ 禁用排序
  446. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  447. return `${params.value} (${params.row.reqUom})`;
  448. },
  449. },
  450. {
  451. field: "reqQty",
  452. headerName: t("Bom Req. Qty"),
  453. flex: 0.7,
  454. align: "right",
  455. headerAlign: "right",
  456. sortable: false, // ✅ 禁用排序
  457. // ✅ 将切换功能移到 header
  458. renderHeader: () => {
  459. const qty = showBaseQty ? t("Base") : t("Req");
  460. const uom = showBaseQty ? t("Base UOM") : t(" ");
  461. return (
  462. <Box
  463. onClick={toggleBaseQty}
  464. sx={{
  465. cursor: "pointer",
  466. userSelect: "none",
  467. width: "100%",
  468. textAlign: "right",
  469. "&:hover": {
  470. textDecoration: "underline",
  471. },
  472. }}
  473. >
  474. {t("Bom Req. Qty")} ({uom})
  475. </Box>
  476. );
  477. },
  478. // ✅ 移除 cell 中的 onClick,只显示值
  479. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  480. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  481. const uom = showBaseQty ? params.row.reqBaseUom : params.row.reqUom;
  482. return (
  483. <Box sx={{ textAlign: "right" }}>
  484. {decimalFormatter.format(qty || 0)} ({uom || ""})
  485. </Box>
  486. );
  487. },
  488. },
  489. {
  490. field: "stockReqQty",
  491. headerName: t("Stock Req. Qty"),
  492. flex: 0.7,
  493. align: "right",
  494. headerAlign: "right",
  495. sortable: false, // ✅ 禁用排序
  496. // ✅ 将切换功能移到 header
  497. renderHeader: () => {
  498. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  499. return (
  500. <Box
  501. onClick={toggleBaseQty}
  502. sx={{
  503. cursor: "pointer",
  504. userSelect: "none",
  505. width: "100%",
  506. textAlign: "right",
  507. "&:hover": {
  508. textDecoration: "underline",
  509. },
  510. }}
  511. >
  512. {t("Stock Req. Qty")} ({uom})
  513. </Box>
  514. );
  515. },
  516. // ✅ 移除 cell 中的 onClick
  517. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  518. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  519. const uom = showBaseQty ? params.row.reqBaseUom : params.row.stockUom;
  520. return (
  521. <Box sx={{ textAlign: "right" }}>
  522. {decimalFormatter.format(qty || 0)} ({uom || ""})
  523. </Box>
  524. );
  525. },
  526. },
  527. {
  528. field: "stockAvailable",
  529. headerName: t("Stock Available"),
  530. flex: 0.7,
  531. align: "right",
  532. headerAlign: "right",
  533. type: "number",
  534. sortable: false, // ✅ 禁用排序
  535. // ✅ 将切换功能移到 header
  536. renderHeader: () => {
  537. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  538. return (
  539. <Box
  540. onClick={toggleBaseQty}
  541. sx={{
  542. cursor: "pointer",
  543. userSelect: "none",
  544. width: "100%",
  545. textAlign: "right",
  546. "&:hover": {
  547. textDecoration: "underline",
  548. },
  549. }}
  550. >
  551. {t("Stock Available")} ({uom})
  552. </Box>
  553. );
  554. },
  555. // ✅ 移除 cell 中的 onClick
  556. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  557. const stockAvailable = getStockAvailable(params.row);
  558. const qty = showBaseQty ? params.row.baseStockQty : (stockAvailable || 0);
  559. const uom = showBaseQty ? params.row.stockBaseUom : params.row.stockUom;
  560. return (
  561. <Box sx={{ textAlign: "right" }}>
  562. {decimalFormatter.format(qty || 0)} ({uom || ""})
  563. </Box>
  564. );
  565. },
  566. },
  567. {
  568. field: "bomProcessSeqNo",
  569. headerName: t("Seq No"),
  570. flex: 0.5,
  571. align: "right",
  572. headerAlign: "right",
  573. type: "number",
  574. sortable: false, // ✅ 禁用排序
  575. },
  576. {
  577. field: "stockStatus",
  578. headerName: t("Stock Status"),
  579. flex: 0.5,
  580. align: "center",
  581. headerAlign: "center",
  582. type: "boolean",
  583. sortable: false, // ✅ 禁用排序
  584. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  585. return isStockSufficient(params.row)
  586. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  587. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  588. },
  589. },
  590. ];
  591. const pickTableRows = jobOrderLines.map((line, index) => ({
  592. ...line,
  593. //id: line.id || index,
  594. id: index + 1,
  595. }));
  596. const PickTableContent = () => (
  597. <Box sx={{ mt: 2 }}>
  598. <ProcessSummaryHeader processData={processData} />
  599. <Card sx={{ mb: 2 }}>
  600. <CardContent>
  601. <Stack
  602. direction="row"
  603. alignItems="center"
  604. justifyContent="space-between"
  605. spacing={2}
  606. >
  607. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  608. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  609. </Typography>
  610. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  611. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  612. </Typography>
  613. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  614. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  615. </Typography>
  616. {fromJosave && (
  617. <Button
  618. variant="contained"
  619. color="error"
  620. onClick={() => handleDeleteJobOrder(jobOrderId)}
  621. disabled={processData?.jobOrderStatus !== "planning"}
  622. >
  623. {t("Delete Job Order")}
  624. </Button>
  625. )}
  626. {fromJosave && (
  627. <Button
  628. variant="contained"
  629. color="primary"
  630. onClick={() => handleRelease(jobOrderId)}
  631. //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  632. disabled={processData?.jobOrderStatus !== "planning"}
  633. >
  634. {t("Release")}
  635. </Button>
  636. )}
  637. </Stack>
  638. </CardContent>
  639. </Card>
  640. <StyledDataGrid
  641. sx={{ "--DataGrid-overlayHeight": "100px" }}
  642. disableColumnMenu
  643. rows={pickTableRows}
  644. columns={pickTableColumns}
  645. getRowHeight={() => "auto"}
  646. />
  647. </Box>
  648. );
  649. const ProductionProcessesLineRemarkTableContent = () => (
  650. <Box sx={{ mt: 2 }}>
  651. <ProcessSummaryHeader processData={processData} />
  652. <StyledDataGrid
  653. sx={{
  654. "--DataGrid-overlayHeight": "100px",
  655. // ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
  656. "& .MuiDataGrid-cell": {
  657. fontSize: "0.875rem", // ✅ Match default body2 size
  658. fontWeight: 500,
  659. },
  660. "& .MuiDataGrid-columnHeader": {
  661. fontSize: "0.875rem", // ✅ Match header size
  662. fontWeight: 600,
  663. },
  664. // ✅ Ensure empty columns are visible
  665. "& .MuiDataGrid-columnHeaders": {
  666. display: "flex",
  667. },
  668. "& .MuiDataGrid-row": {
  669. display: "flex",
  670. },
  671. }}
  672. disableColumnMenu
  673. rows={productionProcessesLineRemarkTableRows ?? []}
  674. columns={productionProcessesLineRemarkTableColumns}
  675. getRowHeight={() => 'auto'}
  676. hideFooter={false} // ✅ Ensure footer is visible
  677. />
  678. </Box>
  679. );
  680. return (
  681. <Box>
  682. {/* 返回按钮 */}
  683. <Box sx={{ mb: 2 }}>
  684. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  685. {t("Back to List")}
  686. </Button>
  687. </Box>
  688. {/* 标签页 */}
  689. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  690. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  691. <Tab label={t("Job Order Info")} />
  692. <Tab label={t("BoM Material")} />
  693. <Tab label={t("Production Process")} />
  694. <Tab label={t("Production Process Line Remark")} />
  695. {/* {!fromJosave && (
  696. <Tab label={t("Matching Stock")} />
  697. )} */}
  698. </Tabs>
  699. </Box>
  700. {/* 标签页内容 */}
  701. <Box sx={{ p: 2 }}>
  702. {tabIndex === 0 && <InfoCardContent />}
  703. {tabIndex === 1 && <PickTableContent />}
  704. {tabIndex === 2 && (
  705. <ProductionProcessDetail
  706. jobOrderId={jobOrderId}
  707. onBack={() => {
  708. // 切换回第一个标签页,或者什么都不做
  709. setTabIndex(0);
  710. }}
  711. fromJosave={fromJosave}
  712. />
  713. )}
  714. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  715. {/* {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} */}
  716. <Dialog
  717. open={openOperationPriorityDialog}
  718. onClose={handleClosePriorityDialog}
  719. fullWidth
  720. maxWidth="xs"
  721. >
  722. <DialogTitle>{t("Update Production Priority")}</DialogTitle>
  723. <DialogContent>
  724. <TextField
  725. autoFocus
  726. margin="dense"
  727. label={t("Production Priority")}
  728. type="number"
  729. fullWidth
  730. value={operationPriority}
  731. onChange={(e) => setOperationPriority(Number(e.target.value))}
  732. />
  733. </DialogContent>
  734. <DialogActions>
  735. <Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
  736. <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
  737. </DialogActions>
  738. </Dialog>
  739. <Dialog
  740. open={openPlanStartDialog}
  741. onClose={handleClosePlanStartDialog}
  742. fullWidth
  743. maxWidth="xs"
  744. >
  745. <DialogTitle>{t("Update Target Production Date")}</DialogTitle>
  746. <DialogContent>
  747. <LocalizationProvider dateAdapter={AdapterDayjs}>
  748. <DatePicker
  749. label={t("Target Production Date")}
  750. value={planStartDate}
  751. onChange={(newValue) => setPlanStartDate(newValue)}
  752. slotProps={{
  753. textField: {
  754. fullWidth: true,
  755. margin: "dense",
  756. autoFocus: true,
  757. }
  758. }}
  759. />
  760. </LocalizationProvider>
  761. </DialogContent>
  762. <DialogActions>
  763. <Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
  764. <Button
  765. variant="contained"
  766. onClick={handleConfirmPlanStart}
  767. disabled={!planStartDate}
  768. >
  769. {t("Save")}
  770. </Button>
  771. </DialogActions>
  772. </Dialog>
  773. <Dialog
  774. open={openReqQtyDialog}
  775. onClose={handleCloseReqQtyDialog}
  776. fullWidth
  777. maxWidth="sm"
  778. >
  779. <DialogTitle>{t("Update Required Quantity")}</DialogTitle>
  780. <DialogContent>
  781. <Stack spacing={2} sx={{ mt: 1 }}>
  782. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  783. <TextField
  784. label={t("Base Qty")}
  785. fullWidth
  786. type="number"
  787. variant="outlined"
  788. value={selectedBomForReqQty?.outputQty || 0}
  789. disabled
  790. InputProps={{
  791. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  792. <InputAdornment position="end">
  793. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  794. {selectedBomForReqQty.outputQtyUom}
  795. </Typography>
  796. </InputAdornment>
  797. ) : null
  798. }}
  799. sx={{ flex: 1 }}
  800. />
  801. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  802. ×
  803. </Typography>
  804. <TextField
  805. label={t("Batch Count")}
  806. fullWidth
  807. type="number"
  808. variant="outlined"
  809. value={reqQtyMultiplier}
  810. onChange={(e) => {
  811. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  812. setReqQtyMultiplier(val);
  813. }}
  814. inputProps={{
  815. min: 1,
  816. step: 1
  817. }}
  818. sx={{ flex: 1 }}
  819. />
  820. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  821. =
  822. </Typography>
  823. <TextField
  824. label={t("Req. Qty")}
  825. fullWidth
  826. variant="outlined"
  827. type="number"
  828. value={selectedBomForReqQty ? (reqQtyMultiplier * selectedBomForReqQty.outputQty) : ""}
  829. disabled
  830. InputProps={{
  831. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  832. <InputAdornment position="end">
  833. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  834. {selectedBomForReqQty.outputQtyUom}
  835. </Typography>
  836. </InputAdornment>
  837. ) : null
  838. }}
  839. sx={{ flex: 1 }}
  840. />
  841. </Box>
  842. </Stack>
  843. </DialogContent>
  844. <DialogActions>
  845. <Button onClick={handleCloseReqQtyDialog}>{t("Cancel")}</Button>
  846. <Button
  847. variant="contained"
  848. onClick={handleConfirmReqQty}
  849. disabled={!selectedBomForReqQty || reqQtyMultiplier < 1}
  850. >
  851. {t("Save")}
  852. </Button>
  853. </DialogActions>
  854. </Dialog>
  855. </Box>
  856. </Box>
  857. );
  858. };
  859. export default ProductionProcessJobOrderDetail;