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

ProductionProcessJobOrderDetail.tsx 30 KiB

4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
1週間前
3ヶ月前
4ヶ月前
2ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1週間前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
4ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
4ヶ月前
4ヶ月前
1ヶ月前
2ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  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. // 只取日期部分,避免时区换算导致前一天/后一天
  177. const dateOnly = String(processData.date).slice(0, 10);
  178. setPlanStartDate(dayjs(dateOnly));
  179. } else {
  180. setPlanStartDate(dayjs());
  181. }
  182. setOpenPlanStartDialog(true);
  183. }, [processData?.date]);
  184. const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  185. setOpenPlanStartDialog(false);
  186. setPlanStartDate(null);
  187. }, []);
  188. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  189. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  190. if (response) {
  191. await fetchData();
  192. }
  193. }, [fetchData]);
  194. const handleConfirmPlanStart = useCallback(async () => {
  195. if (!jobOrderId || !planStartDate) return;
  196. // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
  197. const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
  198. await handleUpdatePlanStart(jobOrderId, dateString);
  199. setOpenPlanStartDialog(false);
  200. setPlanStartDate(null);
  201. }, [jobOrderId, planStartDate, handleUpdatePlanStart]);
  202. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  203. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  204. if (response) {
  205. await fetchData();
  206. }
  207. }, [jobOrderId]);
  208. const handleOpenPriorityDialog = () => {
  209. setOperationPriority(processData?.productionPriority ?? 50);
  210. setOpenOperationPriorityDialog(true);
  211. };
  212. const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  213. setOpenOperationPriorityDialog(false);
  214. };
  215. const handleConfirmPriority = async () => {
  216. if (!processData?.id) return;
  217. await handleUpdateOperationPriority(processData.id, Number(operationPriority));
  218. setOpenOperationPriorityDialog(false);
  219. };
  220. const isStockSufficient = (line: JobOrderLineInfo) => {
  221. if (line.type?.toLowerCase() === "consumables") {
  222. return false;
  223. }
  224. const stockAvailable = getStockAvailable(line);
  225. if (stockAvailable === null) {
  226. return false;
  227. }
  228. return stockAvailable >= line.reqQty;
  229. };
  230. const stockCounts = useMemo(() => {
  231. // 过滤掉 consumables 类型的 lines
  232. const nonConsumablesLines = jobOrderLines.filter(
  233. line => {
  234. const type = line.type?.toLowerCase();
  235. return type !== "consumables" &&
  236. type !== "consumable" && // ✅ 添加单数形式
  237. type !== "cmb" &&
  238. type !== "nm"
  239. }
  240. );
  241. const total = nonConsumablesLines.length;
  242. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  243. return {
  244. total,
  245. sufficient,
  246. insufficient: total - sufficient,
  247. };
  248. }, [jobOrderLines, inventoryData]);
  249. const status = processData?.status?.toLowerCase?.() ?? "";
  250. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  251. const response = await deleteJobOrder(jobOrderId)
  252. if (response) {
  253. //setProcessData(response.entity);
  254. //await fetchData();
  255. onBack();
  256. }
  257. }, [jobOrderId]);
  258. const handleRelease = useCallback(async ( jobOrderId: number) => {
  259. // TODO: 替换为实际的 release 调用
  260. console.log("Release clicked for jobOrderId:", jobOrderId);
  261. const response = await releaseJo({ id: jobOrderId })
  262. if (response) {
  263. //setProcessData(response.entity);
  264. await fetchData();
  265. }
  266. }, [jobOrderId]);
  267. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  268. (_e, newValue) => {
  269. setTabIndex(newValue);
  270. },
  271. [],
  272. );
  273. // 如果选择了 process detail,显示 detail 页面
  274. if (selectedProcessId !== null) {
  275. return (
  276. <ProductionProcessDetail
  277. jobOrderId={selectedProcessId}
  278. onBack={() => {
  279. setSelectedProcessId(null);
  280. fetchData(); // 刷新数据
  281. }}
  282. />
  283. );
  284. }
  285. if (loading) {
  286. return (
  287. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  288. <CircularProgress/>
  289. </Box>
  290. );
  291. }
  292. if (!processData) {
  293. return (
  294. <Box>
  295. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  296. {t("Back")}
  297. </Button>
  298. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  299. </Box>
  300. );
  301. }
  302. // InfoCard 组件内容
  303. const InfoCardContent = () => (
  304. <Card sx={{ display: "block", mt: 2 }}>
  305. <CardContent component={Stack} spacing={4}>
  306. <Box>
  307. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  308. <Grid item xs={6}>
  309. <TextField
  310. label={t("Job Order Code")}
  311. fullWidth
  312. disabled={true}
  313. value={processData?.jobOrderCode || ""}
  314. />
  315. </Grid>
  316. <Grid item xs={6}>
  317. <TextField
  318. label={t("Item Code")}
  319. fullWidth
  320. disabled={true}
  321. value={processData?.itemCode+"-"+processData?.itemName || ""}
  322. />
  323. </Grid>
  324. <Grid item xs={6}>
  325. <TextField
  326. label={t("Job Type")}
  327. fullWidth
  328. disabled={true}
  329. value={t(processData?.jobType) || t("N/A")}
  330. //value={t("N/A")}
  331. />
  332. </Grid>
  333. <Grid item xs={6}>
  334. <TextField
  335. label={t("Req. Qty")}
  336. fullWidth
  337. disabled={true}
  338. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  339. InputProps={{
  340. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  341. <InputAdornment position="end">
  342. <IconButton size="small" onClick={handleOpenReqQtyDialog}>
  343. <EditIcon fontSize="small" />
  344. </IconButton>
  345. </InputAdornment>
  346. ) : null),
  347. }}
  348. />
  349. </Grid>
  350. <Grid item xs={6}>
  351. <TextField
  352. value={processData?.date ? String(processData.date).slice(0, 10) : ""}
  353. label={t("Target Production Date")}
  354. fullWidth
  355. disabled={true}
  356. InputProps={{
  357. endAdornment: (processData?.jobOrderStatus === "planning" ? (
  358. <InputAdornment position="end">
  359. <IconButton size="small" onClick={handleOpenPlanStartDialog}>
  360. <EditIcon fontSize="small" />
  361. </IconButton>
  362. </InputAdornment>
  363. ) : null),
  364. }}
  365. />
  366. </Grid>
  367. <Grid item xs={6}>
  368. <TextField
  369. label={t("Production Priority")}
  370. fullWidth
  371. disabled={true}
  372. value={processData?.productionPriority ?? "50"}
  373. InputProps={{
  374. endAdornment: (
  375. <InputAdornment position="end">
  376. <IconButton size="small" onClick={handleOpenPriorityDialog}>
  377. <EditIcon fontSize="small" />
  378. </IconButton>
  379. </InputAdornment>
  380. ),
  381. }}
  382. />
  383. </Grid>
  384. <Grid item xs={6}>
  385. <TextField
  386. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance | Time Sequence | Complexity")}
  387. fullWidth
  388. disabled={true}
  389. 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}`}
  390. />
  391. </Grid>
  392. </Grid>
  393. </Box>
  394. </CardContent>
  395. </Card>
  396. );
  397. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  398. {
  399. field: "seqNo",
  400. headerName: t("SEQ"),
  401. flex: 0.2,
  402. align: "left",
  403. headerAlign: "left",
  404. type: "number",
  405. renderCell: (params) => {
  406. return <Typography sx={{ fontWeight: 500 }}>{params.value}</Typography>;
  407. },
  408. },
  409. {
  410. field: "description",
  411. headerName: t("Remark"),
  412. flex: 1,
  413. align: "left",
  414. headerAlign: "left",
  415. renderCell: (params) => {
  416. return(
  417. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  418. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  419. <Typography sx={{ fontWeight: 500 }}>{params.value || ""}</Typography>
  420. <Typography sx={{ fontWeight: 500 }}>&nbsp;</Typography>
  421. </Box>
  422. )
  423. },
  424. },
  425. ];
  426. const productionProcessesLineRemarkTableRows =
  427. processData?.productProcessLines?.map((line: any) => ({
  428. id: line.seqNo,
  429. seqNo: line.seqNo,
  430. description: line.description ?? "",
  431. })) ?? [];
  432. const pickTableColumns: GridColDef[] = [
  433. {
  434. field: "id",
  435. headerName: t("id"),
  436. flex: 0.2,
  437. align: "left",
  438. headerAlign: "left",
  439. type: "number",
  440. sortable: false, // ✅ 禁用排序
  441. },
  442. {
  443. field: "itemCode",
  444. headerName: t("Material Code"),
  445. flex: 0.6,
  446. sortable: false,
  447. },
  448. {
  449. field: "itemName",
  450. headerName: t("Item Name"),
  451. flex: 1,
  452. sortable: false, // ✅ 禁用排序
  453. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  454. return `${params.value} (${params.row.reqUom})`;
  455. },
  456. },
  457. {
  458. field: "reqQty",
  459. headerName: t("Bom Req. Qty"),
  460. flex: 0.7,
  461. align: "right",
  462. headerAlign: "right",
  463. sortable: false,
  464. renderHeader: () => {
  465. const uom = showBaseQty ? t("Base UOM") : t("Bom Uom");
  466. return (
  467. <Box
  468. onClick={toggleBaseQty}
  469. sx={{
  470. cursor: "pointer",
  471. userSelect: "none",
  472. width: "100%",
  473. textAlign: "right",
  474. "&:hover": {
  475. textDecoration: "underline",
  476. },
  477. }}
  478. >
  479. <Typography variant="body2">
  480. {t("Bom Req. Qty")}<br/>
  481. ({uom})
  482. </Typography>
  483. </Box>
  484. );
  485. },
  486. // ✅ 移除 cell 中的 onClick,只显示值
  487. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  488. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  489. const uom = showBaseQty ? params.row.reqBaseUom : params.row.reqUom;
  490. return (
  491. <Box sx={{ textAlign: "right" }}>
  492. {decimalFormatter.format(qty || 0)} ({uom || ""})
  493. </Box>
  494. );
  495. },
  496. },
  497. {
  498. field: "stockReqQty",
  499. headerName: t("Stock Req. Qty"),
  500. flex: 0.7,
  501. align: "right",
  502. headerAlign: "right",
  503. sortable: false, // ✅ 禁用排序
  504. // ✅ 将切换功能移到 header
  505. renderHeader: () => {
  506. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  507. return (
  508. <Box
  509. onClick={toggleBaseQty}
  510. sx={{
  511. cursor: "pointer",
  512. userSelect: "none",
  513. width: "100%",
  514. textAlign: "right",
  515. "&:hover": {
  516. textDecoration: "underline",
  517. },
  518. }}
  519. >
  520. <Typography variant="body2">
  521. {t("Stock Req. Qty")} <br/>
  522. ({uom})
  523. </Typography>
  524. </Box>
  525. );
  526. },
  527. // ✅ 移除 cell 中的 onClick
  528. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  529. const qty = showBaseQty ? params.row.baseReqQty : params.value;
  530. const uom = showBaseQty ? params.row.reqBaseUom : params.row.stockUom;
  531. return (
  532. <Box sx={{ textAlign: "right" }}>
  533. {decimalFormatter.format(qty || 0)} ({uom || ""})
  534. </Box>
  535. );
  536. },
  537. },
  538. {
  539. field: "stockAvailable",
  540. headerName: t("Stock Available"),
  541. flex: 0.7,
  542. align: "right",
  543. headerAlign: "right",
  544. type: "number",
  545. sortable: false, // ✅ 禁用排序
  546. // ✅ 将切换功能移到 header
  547. renderHeader: () => {
  548. const uom = showBaseQty ? t("Base UOM") : t("Stock UOM");
  549. return (
  550. <Box
  551. onClick={toggleBaseQty}
  552. sx={{
  553. cursor: "pointer",
  554. userSelect: "none",
  555. width: "100%",
  556. textAlign: "right",
  557. "&:hover": {
  558. textDecoration: "underline",
  559. },
  560. }}
  561. >
  562. <Typography variant="body2">
  563. {t("Stock Available")} <br/>
  564. ({uom})
  565. </Typography>
  566. </Box>
  567. );
  568. },
  569. // ✅ 移除 cell 中的 onClick
  570. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  571. const stockAvailable = getStockAvailable(params.row);
  572. const qty = showBaseQty ? params.row.baseStockQty : (stockAvailable || 0);
  573. const uom = showBaseQty ? params.row.stockBaseUom : params.row.stockUom;
  574. return (
  575. <Box sx={{ textAlign: "right" }}>
  576. {decimalFormatter.format(qty || 0)} ({uom || ""})
  577. </Box>
  578. );
  579. },
  580. },
  581. {
  582. field: "bomProcessSeqNo",
  583. headerName: t("Seq No"),
  584. flex: 0.5,
  585. align: "right",
  586. headerAlign: "right",
  587. type: "number",
  588. sortable: false, // ✅ 禁用排序
  589. },
  590. {
  591. field: "stockStatus",
  592. headerName: t("Stock Status"),
  593. flex: 0.5,
  594. align: "center",
  595. headerAlign: "center",
  596. type: "boolean",
  597. sortable: false, // ✅ 禁用排序
  598. renderCell: (params: GridRenderCellParams<JobOrderLineInfo>) => {
  599. return isStockSufficient(params.row)
  600. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  601. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  602. },
  603. },
  604. ];
  605. const pickTableRows = jobOrderLines.map((line, index) => ({
  606. ...line,
  607. //id: line.id || index,
  608. id: index + 1,
  609. }));
  610. const PickTableContent = () => (
  611. <Box sx={{ mt: 2 }}>
  612. <ProcessSummaryHeader processData={processData} />
  613. <Card sx={{ mb: 2 }}>
  614. <CardContent>
  615. <Stack
  616. direction="row"
  617. alignItems="center"
  618. justifyContent="space-between"
  619. spacing={2}
  620. >
  621. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  622. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  623. </Typography>
  624. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  625. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  626. </Typography>
  627. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  628. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  629. </Typography>
  630. {fromJosave && (
  631. <Button
  632. variant="contained"
  633. color="error"
  634. onClick={() => handleDeleteJobOrder(jobOrderId)}
  635. disabled={processData?.jobOrderStatus !== "planning"}
  636. >
  637. {t("Delete Job Order")}
  638. </Button>
  639. )}
  640. {fromJosave && (
  641. <Button
  642. variant="contained"
  643. color="primary"
  644. onClick={() => handleRelease(jobOrderId)}
  645. //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  646. disabled={processData?.jobOrderStatus !== "planning"}
  647. >
  648. {t("Release")}
  649. </Button>
  650. )}
  651. </Stack>
  652. </CardContent>
  653. </Card>
  654. <StyledDataGrid
  655. sx={{ "--DataGrid-overlayHeight": "200px" }}
  656. disableColumnMenu
  657. rows={pickTableRows}
  658. columns={pickTableColumns}
  659. getRowHeight={() => "auto"}
  660. />
  661. </Box>
  662. );
  663. const ProductionProcessesLineRemarkTableContent = () => (
  664. <Box sx={{ mt: 2 }}>
  665. <ProcessSummaryHeader processData={processData} />
  666. <StyledDataGrid
  667. sx={{
  668. "--DataGrid-overlayHeight": "100px",
  669. // ✅ Match ProductionProcessDetail font size (default body2 = 0.875rem)
  670. "& .MuiDataGrid-cell": {
  671. fontSize: "0.875rem", // ✅ Match default body2 size
  672. fontWeight: 500,
  673. },
  674. "& .MuiDataGrid-columnHeader": {
  675. fontSize: "0.875rem", // ✅ Match header size
  676. fontWeight: 600,
  677. },
  678. // ✅ Ensure empty columns are visible
  679. "& .MuiDataGrid-columnHeaders": {
  680. display: "flex",
  681. },
  682. "& .MuiDataGrid-row": {
  683. display: "flex",
  684. },
  685. }}
  686. disableColumnMenu
  687. rows={productionProcessesLineRemarkTableRows ?? []}
  688. columns={productionProcessesLineRemarkTableColumns}
  689. getRowHeight={() => 'auto'}
  690. hideFooter={false} // ✅ Ensure footer is visible
  691. />
  692. </Box>
  693. );
  694. return (
  695. <Box>
  696. {/* 返回按钮 */}
  697. <Box sx={{ mb: 2 }}>
  698. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  699. {t("Back to List")}
  700. </Button>
  701. </Box>
  702. {/* 标签页 */}
  703. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  704. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  705. <Tab label={t("Job Order Info")} />
  706. <Tab label={t("BoM Material")} />
  707. <Tab label={t("Production Process")} />
  708. <Tab label={t("Production Process Line Remark")} />
  709. {/* {!fromJosave && (
  710. <Tab label={t("Matching Stock")} />
  711. )} */}
  712. </Tabs>
  713. </Box>
  714. {/* 标签页内容 */}
  715. <Box sx={{ p: 2 }}>
  716. {tabIndex === 0 && <InfoCardContent />}
  717. {tabIndex === 1 && <PickTableContent />}
  718. {tabIndex === 2 && (
  719. <ProductionProcessDetail
  720. jobOrderId={jobOrderId}
  721. onBack={() => {
  722. // 切换回第一个标签页,或者什么都不做
  723. setTabIndex(0);
  724. }}
  725. fromJosave={fromJosave}
  726. />
  727. )}
  728. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  729. {/* {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} */}
  730. <Dialog
  731. open={openOperationPriorityDialog}
  732. onClose={handleClosePriorityDialog}
  733. fullWidth
  734. maxWidth="xs"
  735. >
  736. <DialogTitle>{t("Update Production Priority")}</DialogTitle>
  737. <DialogContent>
  738. <TextField
  739. autoFocus
  740. margin="dense"
  741. label={t("Production Priority")}
  742. type="number"
  743. fullWidth
  744. value={operationPriority}
  745. onChange={(e) => setOperationPriority(Number(e.target.value))}
  746. />
  747. </DialogContent>
  748. <DialogActions>
  749. <Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
  750. <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
  751. </DialogActions>
  752. </Dialog>
  753. <Dialog
  754. open={openPlanStartDialog}
  755. onClose={handleClosePlanStartDialog}
  756. fullWidth
  757. maxWidth="xs"
  758. >
  759. <DialogTitle>{t("Update Target Production Date")}</DialogTitle>
  760. <DialogContent>
  761. <LocalizationProvider dateAdapter={AdapterDayjs}>
  762. <DatePicker
  763. label={t("Target Production Date")}
  764. value={planStartDate}
  765. onChange={(newValue) => setPlanStartDate(newValue)}
  766. slotProps={{
  767. textField: {
  768. fullWidth: true,
  769. margin: "dense",
  770. autoFocus: true,
  771. }
  772. }}
  773. />
  774. </LocalizationProvider>
  775. </DialogContent>
  776. <DialogActions>
  777. <Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
  778. <Button
  779. variant="contained"
  780. onClick={handleConfirmPlanStart}
  781. disabled={!planStartDate}
  782. >
  783. {t("Save")}
  784. </Button>
  785. </DialogActions>
  786. </Dialog>
  787. <Dialog
  788. open={openReqQtyDialog}
  789. onClose={handleCloseReqQtyDialog}
  790. fullWidth
  791. maxWidth="sm"
  792. >
  793. <DialogTitle>{t("Update Required Quantity")}</DialogTitle>
  794. <DialogContent>
  795. <Stack spacing={2} sx={{ mt: 1 }}>
  796. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  797. <TextField
  798. label={t("Base Qty")}
  799. fullWidth
  800. type="number"
  801. variant="outlined"
  802. value={selectedBomForReqQty?.outputQty || 0}
  803. disabled
  804. InputProps={{
  805. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  806. <InputAdornment position="end">
  807. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  808. {selectedBomForReqQty.outputQtyUom}
  809. </Typography>
  810. </InputAdornment>
  811. ) : null
  812. }}
  813. sx={{ flex: 1 }}
  814. />
  815. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  816. ×
  817. </Typography>
  818. <TextField
  819. label={t("Batch Count")}
  820. fullWidth
  821. type="number"
  822. variant="outlined"
  823. value={reqQtyMultiplier}
  824. onChange={(e) => {
  825. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  826. setReqQtyMultiplier(val);
  827. }}
  828. inputProps={{
  829. min: 1,
  830. step: 1
  831. }}
  832. sx={{ flex: 1 }}
  833. />
  834. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  835. =
  836. </Typography>
  837. <TextField
  838. label={t("Req. Qty")}
  839. fullWidth
  840. variant="outlined"
  841. type="number"
  842. value={selectedBomForReqQty ? (reqQtyMultiplier * selectedBomForReqQty.outputQty) : ""}
  843. disabled
  844. InputProps={{
  845. endAdornment: selectedBomForReqQty?.outputQtyUom ? (
  846. <InputAdornment position="end">
  847. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  848. {selectedBomForReqQty.outputQtyUom}
  849. </Typography>
  850. </InputAdornment>
  851. ) : null
  852. }}
  853. sx={{ flex: 1 }}
  854. />
  855. </Box>
  856. </Stack>
  857. </DialogContent>
  858. <DialogActions>
  859. <Button onClick={handleCloseReqQtyDialog}>{t("Cancel")}</Button>
  860. <Button
  861. variant="contained"
  862. onClick={handleConfirmReqQty}
  863. disabled={!selectedBomForReqQty || reqQtyMultiplier < 1}
  864. >
  865. {t("Save")}
  866. </Button>
  867. </DialogActions>
  868. </Dialog>
  869. </Box>
  870. </Box>
  871. );
  872. };
  873. export default ProductionProcessJobOrderDetail;