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

ProductionProcessJobOrderDetail.tsx 16 KiB

1ヶ月前
4週間前
1ヶ月前
2週間前
1ヶ月前
3週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
3週間前
1ヶ月前
3週間前
1ヶ月前
4週間前
2週間前
4週間前
2週間前
4週間前
2週間前
4週間前
2週間前
2週間前
4週間前
2週間前
2週間前
2週間前
3週間前
4週間前
3週間前
2週間前
3週間前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
3週間前
3週間前
3週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
3週間前
1ヶ月前
3週間前
1ヶ月前
2週間前
1ヶ月前
4週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
3週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
2週間前
1ヶ月前
2週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
3週間前
4週間前
3週間前
4週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
4週間前
2週間前
4週間前
3週間前
4週間前
3週間前
3週間前
4週間前
1ヶ月前
4週間前
1ヶ月前
4週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
4週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
1ヶ月前
3週間前
1ヶ月前
2週間前
1ヶ月前
2週間前
1ヶ月前
3週間前
1ヶ月前
3週間前
1ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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. } from "@mui/material";
  18. import ArrowBackIcon from '@mui/icons-material/ArrowBack';
  19. import { useTranslation } from "react-i18next";
  20. import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions";
  21. import ProductionProcessDetail from "./ProductionProcessDetail";
  22. import dayjs from "dayjs";
  23. import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
  24. import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
  25. import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
  26. import { decimalFormatter } from "@/app/utils/formatUtil";
  27. import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
  28. import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
  29. import { fetchInventories } from "@/app/api/inventory/actions";
  30. import { InventoryResult } from "@/app/api/inventory";
  31. import { releaseJo, startJo } from "@/app/api/jo/actions";
  32. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  33. import ProcessSummaryHeader from "./ProcessSummaryHeader";
  34. interface JobOrderLine {
  35. id: number;
  36. jobOrderId: number;
  37. jobOrderCode: string;
  38. itemId: number;
  39. itemCode: string;
  40. itemName: string;
  41. reqQty: number;
  42. stockQty: number;
  43. uom: string;
  44. shortUom: string;
  45. availableStatus: string;
  46. type: string;
  47. }
  48. interface ProductProcessJobOrderDetailProps {
  49. jobOrderId: number;
  50. onBack: () => void;
  51. fromJosave?: boolean;
  52. }
  53. const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
  54. jobOrderId,
  55. onBack,
  56. fromJosave,
  57. }) => {
  58. const { t } = useTranslation();
  59. const [loading, setLoading] = useState(false);
  60. const [processData, setProcessData] = useState<any>(null);
  61. const [jobOrderLines, setJobOrderLines] = useState<JobOrderLine[]>([]);
  62. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  63. const [tabIndex, setTabIndex] = useState(0);
  64. const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
  65. // 获取数据
  66. const fetchData = useCallback(async () => {
  67. setLoading(true);
  68. try {
  69. const data = await fetchProductProcessesByJobOrderId(jobOrderId);
  70. if (data && data.length > 0) {
  71. const firstProcess = data[0];
  72. setProcessData(firstProcess);
  73. setJobOrderLines((firstProcess as any).jobOrderLines || []);
  74. }
  75. } catch (error) {
  76. console.error("Error loading data:", error);
  77. } finally {
  78. setLoading(false);
  79. }
  80. }, [jobOrderId]);
  81. // 获取库存数据
  82. useEffect(() => {
  83. const fetchInventoryData = async () => {
  84. try {
  85. const inventoryResponse = await fetchInventories({
  86. code: "",
  87. name: "",
  88. type: "",
  89. pageNum: 0,
  90. pageSize: 1000
  91. });
  92. setInventoryData(inventoryResponse.records);
  93. } catch (error) {
  94. console.error("Error fetching inventory data:", error);
  95. }
  96. };
  97. fetchInventoryData();
  98. }, []);
  99. useEffect(() => {
  100. fetchData();
  101. }, [fetchData]);
  102. // PickTable 组件内容
  103. const getStockAvailable = (line: JobOrderLine) => {
  104. if (line.type?.toLowerCase() === "consumables") {
  105. return null;
  106. }
  107. const inventory = inventoryData.find(inv =>
  108. inv.itemCode === line.itemCode || inv.itemName === line.itemName
  109. );
  110. if (inventory) {
  111. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  112. }
  113. return line.stockQty || 0;
  114. };
  115. const isStockSufficient = (line: JobOrderLine) => {
  116. if (line.type?.toLowerCase() === "consumables") {
  117. return false;
  118. }
  119. const stockAvailable = getStockAvailable(line);
  120. if (stockAvailable === null) {
  121. return false;
  122. }
  123. return stockAvailable >= line.reqQty;
  124. };
  125. const stockCounts = useMemo(() => {
  126. // 过滤掉 consumables 类型的 lines
  127. const nonConsumablesLines = jobOrderLines.filter(
  128. line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb"
  129. );
  130. const total = nonConsumablesLines.length;
  131. const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
  132. return {
  133. total,
  134. sufficient,
  135. insufficient: total - sufficient,
  136. };
  137. }, [jobOrderLines, inventoryData]);
  138. const status = processData?.status?.toLowerCase?.() ?? "";
  139. const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
  140. const response = await deleteJobOrder(jobOrderId)
  141. if (response) {
  142. //setProcessData(response.entity);
  143. await fetchData();
  144. }
  145. }, [jobOrderId]);
  146. const handleRelease = useCallback(async ( jobOrderId: number) => {
  147. // TODO: 替换为实际的 release 调用
  148. console.log("Release clicked for jobOrderId:", jobOrderId);
  149. const response = await releaseJo({ id: jobOrderId })
  150. if (response) {
  151. //setProcessData(response.entity);
  152. await fetchData();
  153. }
  154. }, [jobOrderId]);
  155. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  156. (_e, newValue) => {
  157. setTabIndex(newValue);
  158. },
  159. [],
  160. );
  161. // 如果选择了 process detail,显示 detail 页面
  162. if (selectedProcessId !== null) {
  163. return (
  164. <ProductionProcessDetail
  165. jobOrderId={selectedProcessId}
  166. onBack={() => {
  167. setSelectedProcessId(null);
  168. fetchData(); // 刷新数据
  169. }}
  170. />
  171. );
  172. }
  173. if (loading) {
  174. return (
  175. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  176. <CircularProgress/>
  177. </Box>
  178. );
  179. }
  180. if (!processData) {
  181. return (
  182. <Box>
  183. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  184. {t("Back")}
  185. </Button>
  186. <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
  187. </Box>
  188. );
  189. }
  190. // InfoCard 组件内容
  191. const InfoCardContent = () => (
  192. <Card sx={{ display: "block", mt: 2 }}>
  193. <CardContent component={Stack} spacing={4}>
  194. <Box>
  195. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  196. <Grid item xs={6}>
  197. <TextField
  198. label={t("Job Order Code")}
  199. fullWidth
  200. disabled={true}
  201. value={processData?.jobOrderCode || ""}
  202. />
  203. </Grid>
  204. <Grid item xs={6}>
  205. <TextField
  206. label={t("Item Code")}
  207. fullWidth
  208. disabled={true}
  209. value={processData?.itemCode+"-"+processData?.itemName || ""}
  210. />
  211. </Grid>
  212. <Grid item xs={6}>
  213. <TextField
  214. label={t("Job Type")}
  215. fullWidth
  216. disabled={true}
  217. value={t(processData?.jobType) || t("N/A")}
  218. //value={t("N/A")}
  219. />
  220. </Grid>
  221. <Grid item xs={6}>
  222. <TextField
  223. label={t("Req. Qty")}
  224. fullWidth
  225. disabled={true}
  226. value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
  227. />
  228. </Grid>
  229. <Grid item xs={6}>
  230. <TextField
  231. value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}
  232. label={t("Target Production Date")}
  233. fullWidth
  234. disabled={true}
  235. />
  236. </Grid>
  237. <Grid item xs={6}>
  238. <TextField
  239. label={t("Production Priority")}
  240. fullWidth
  241. disabled={true}
  242. value={processData?.productionPriority ||processData?.isDense === 0 ? "50" : processData?.productionPriority || "0"}
  243. />
  244. </Grid>
  245. <Grid item xs={6}>
  246. <TextField
  247. label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance")}
  248. fullWidth
  249. disabled={true}
  250. 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)}`}
  251. />
  252. </Grid>
  253. </Grid>
  254. </Box>
  255. </CardContent>
  256. </Card>
  257. );
  258. const productionProcessesLineRemarkTableColumns: GridColDef[] = [
  259. {
  260. field: "seqNo",
  261. headerName: t("Seq"),
  262. flex: 0.2,
  263. align: "left",
  264. headerAlign: "center",
  265. type: "number",
  266. renderCell: (params) => {
  267. return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>;
  268. },
  269. },
  270. {
  271. field: "description",
  272. headerName: t("Remark"),
  273. flex: 1,
  274. align: "left",
  275. headerAlign: "center",
  276. renderCell: (params) => {
  277. return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>;
  278. },
  279. },
  280. ];
  281. const productionProcessesLineRemarkTableRows =
  282. processData?.productProcessLines?.map((line: any) => ({
  283. id: line.seqNo,
  284. seqNo: line.seqNo,
  285. description: line.description ?? "",
  286. })) ?? [];
  287. const pickTableColumns: GridColDef[] = [
  288. {
  289. field: "id",
  290. headerName: t("id"),
  291. flex: 0.2,
  292. align: "left",
  293. headerAlign: "left",
  294. type: "number",
  295. },
  296. {
  297. field: "itemCode",
  298. headerName: t("Item Code"),
  299. flex: 0.6,
  300. },
  301. {
  302. field: "itemName",
  303. headerName: t("Item Name"),
  304. flex: 1,
  305. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  306. return `${params.value} (${params.row.uom})`;
  307. },
  308. },
  309. {
  310. field: "reqQty",
  311. headerName: t("Req. Qty"),
  312. flex: 0.7,
  313. align: "right",
  314. headerAlign: "right",
  315. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  316. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  317. return t("N/A");
  318. }
  319. return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`;
  320. },
  321. },
  322. {
  323. field: "stockAvailable",
  324. headerName: t("Stock Available"),
  325. flex: 0.7,
  326. align: "right",
  327. headerAlign: "right",
  328. type: "number",
  329. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  330. // 如果是 consumables,显示 N/A
  331. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  332. return t("N/A");
  333. }
  334. const stockAvailable = getStockAvailable(params.row);
  335. if (stockAvailable === null) {
  336. return t("N/A");
  337. }
  338. return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`;
  339. },
  340. },
  341. {
  342. field: "bomProcessSeqNo",
  343. headerName: t("Seq No"),
  344. flex: 0.5,
  345. align: "right",
  346. headerAlign: "right",
  347. type: "number",
  348. },
  349. /*
  350. {
  351. field: "seqNoRemark",
  352. headerName: t("Seq No Remark"),
  353. flex: 1,
  354. align: "left",
  355. headerAlign: "left",
  356. type: "string",
  357. },
  358. */
  359. {
  360. field: "stockStatus",
  361. headerName: t("Stock Status"),
  362. flex: 0.5,
  363. align: "center",
  364. headerAlign: "center",
  365. type: "boolean",
  366. renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
  367. if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
  368. return <Typography>{t("N/A")}</Typography>;
  369. }
  370. return isStockSufficient(params.row)
  371. ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
  372. : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
  373. },
  374. },
  375. ];
  376. const pickTableRows = jobOrderLines.map((line, index) => ({
  377. ...line,
  378. //id: line.id || index,
  379. id: index + 1,
  380. }));
  381. const PickTableContent = () => (
  382. <Box sx={{ mt: 2 }}>
  383. <ProcessSummaryHeader processData={processData} />
  384. <Card sx={{ mb: 2 }}>
  385. <CardContent>
  386. <Stack
  387. direction="row"
  388. alignItems="center"
  389. justifyContent="space-between"
  390. spacing={2}
  391. >
  392. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  393. {t("Total lines: ")}<strong>{stockCounts.total}</strong>
  394. </Typography>
  395. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  396. {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
  397. </Typography>
  398. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  399. {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
  400. </Typography>
  401. {fromJosave && (
  402. <Button
  403. variant="contained"
  404. color="error"
  405. onClick={() => handleDeleteJobOrder(jobOrderId)}
  406. disabled={processData?.jobOrderStatus !== "planning"}
  407. >
  408. {t("Delete Job Order")}
  409. </Button>
  410. )}
  411. {fromJosave && (
  412. <Button
  413. variant="contained"
  414. color="primary"
  415. onClick={() => handleRelease(jobOrderId)}
  416. disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
  417. >
  418. {t("Release")}
  419. </Button>
  420. )}
  421. </Stack>
  422. </CardContent>
  423. </Card>
  424. <StyledDataGrid
  425. sx={{ "--DataGrid-overlayHeight": "100px" }}
  426. disableColumnMenu
  427. rows={pickTableRows}
  428. columns={pickTableColumns}
  429. getRowHeight={() => "auto"}
  430. />
  431. </Box>
  432. );
  433. const ProductionProcessesLineRemarkTableContent = () => (
  434. <Box sx={{ mt: 2 }}>
  435. <ProcessSummaryHeader processData={processData} />
  436. <StyledDataGrid
  437. sx={{
  438. "--DataGrid-overlayHeight": "100px",
  439. }}
  440. disableColumnMenu
  441. rows={productionProcessesLineRemarkTableRows ?? []}
  442. columns={productionProcessesLineRemarkTableColumns}
  443. getRowHeight={() => 'auto'}
  444. />
  445. </Box>
  446. );
  447. return (
  448. <Box>
  449. {/* 返回按钮 */}
  450. <Box sx={{ mb: 2 }}>
  451. <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
  452. {t("Back to List")}
  453. </Button>
  454. </Box>
  455. {/* 标签页 */}
  456. <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
  457. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  458. <Tab label={t("Job Order Info")} />
  459. <Tab label={t("BoM Material")} />
  460. <Tab label={t("Production Process")} />
  461. <Tab label={t("Production Process Line Remark")} />
  462. {!fromJosave && (
  463. <Tab label={t("Matching Stock")} />
  464. )}
  465. </Tabs>
  466. </Box>
  467. {/* 标签页内容 */}
  468. <Box sx={{ p: 2 }}>
  469. {tabIndex === 0 && <InfoCardContent />}
  470. {tabIndex === 1 && <PickTableContent />}
  471. {tabIndex === 2 && (
  472. <ProductionProcessDetail
  473. jobOrderId={jobOrderId}
  474. onBack={() => {
  475. // 切换回第一个标签页,或者什么都不做
  476. setTabIndex(0);
  477. }}
  478. fromJosave={fromJosave}
  479. />
  480. )}
  481. {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
  482. {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />}
  483. </Box>
  484. </Box>
  485. );
  486. };
  487. export default ProductionProcessJobOrderDetail;