|
- "use client";
- import React, { useCallback, useEffect, useState, useMemo } from "react";
- import {
- Box,
- Button,
- Paper,
- Stack,
- Typography,
- TextField,
- Grid,
- Card,
- CardContent,
- CircularProgress,
- Tabs,
- Tab,
- TabsProps,
- } from "@mui/material";
- import ArrowBackIcon from '@mui/icons-material/ArrowBack';
- import { useTranslation } from "react-i18next";
- import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions";
- import ProductionProcessDetail from "./ProductionProcessDetail";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
- import StyledDataGrid from "../StyledDataGrid/StyledDataGrid";
- import { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
- import { decimalFormatter } from "@/app/utils/formatUtil";
- import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
- import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded';
- import { fetchInventories } from "@/app/api/inventory/actions";
- import { InventoryResult } from "@/app/api/inventory";
- import { releaseJo, startJo } from "@/app/api/jo/actions";
- import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
- import ProcessSummaryHeader from "./ProcessSummaryHeader";
- interface JobOrderLine {
- id: number;
- jobOrderId: number;
- jobOrderCode: string;
- itemId: number;
- itemCode: string;
- itemName: string;
- reqQty: number;
- stockQty: number;
- uom: string;
- shortUom: string;
- availableStatus: string;
- type: string;
- }
-
- interface ProductProcessJobOrderDetailProps {
- jobOrderId: number;
- onBack: () => void;
- fromJosave?: boolean;
- }
-
- const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProps> = ({
- jobOrderId,
- onBack,
- fromJosave,
- }) => {
- const { t } = useTranslation();
- const [loading, setLoading] = useState(false);
- const [processData, setProcessData] = useState<any>(null);
- const [jobOrderLines, setJobOrderLines] = useState<JobOrderLine[]>([]);
- const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
- const [tabIndex, setTabIndex] = useState(0);
- const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
-
- // 获取数据
- const fetchData = useCallback(async () => {
- setLoading(true);
- try {
- const data = await fetchProductProcessesByJobOrderId(jobOrderId);
- if (data && data.length > 0) {
- const firstProcess = data[0];
- setProcessData(firstProcess);
- setJobOrderLines((firstProcess as any).jobOrderLines || []);
- }
- } catch (error) {
- console.error("Error loading data:", error);
- } finally {
- setLoading(false);
- }
- }, [jobOrderId]);
-
- // 获取库存数据
- useEffect(() => {
- const fetchInventoryData = async () => {
- try {
- const inventoryResponse = await fetchInventories({
- code: "",
- name: "",
- type: "",
- pageNum: 0,
- pageSize: 1000
- });
- setInventoryData(inventoryResponse.records);
- } catch (error) {
- console.error("Error fetching inventory data:", error);
- }
- };
- fetchInventoryData();
- }, []);
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
- // PickTable 组件内容
- const getStockAvailable = (line: JobOrderLine) => {
- if (line.type?.toLowerCase() === "consumables") {
- return null;
- }
- const inventory = inventoryData.find(inv =>
- inv.itemCode === line.itemCode || inv.itemName === line.itemName
- );
- if (inventory) {
- return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
- }
- return line.stockQty || 0;
- };
-
- const isStockSufficient = (line: JobOrderLine) => {
- if (line.type?.toLowerCase() === "consumables") {
- return false;
- }
- const stockAvailable = getStockAvailable(line);
- if (stockAvailable === null) {
- return false;
- }
- return stockAvailable >= line.reqQty;
- };
- const stockCounts = useMemo(() => {
- // 过滤掉 consumables 类型的 lines
- const nonConsumablesLines = jobOrderLines.filter(
- line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb"
- );
- const total = nonConsumablesLines.length;
- const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
- return {
- total,
- sufficient,
- insufficient: total - sufficient,
- };
- }, [jobOrderLines, inventoryData]);
- const status = processData?.status?.toLowerCase?.() ?? "";
- const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => {
- const response = await deleteJobOrder(jobOrderId)
- if (response) {
- //setProcessData(response.entity);
- await fetchData();
- }
- }, [jobOrderId]);
- const handleRelease = useCallback(async ( jobOrderId: number) => {
- // TODO: 替换为实际的 release 调用
- console.log("Release clicked for jobOrderId:", jobOrderId);
- const response = await releaseJo({ id: jobOrderId })
- if (response) {
- //setProcessData(response.entity);
- await fetchData();
- }
- }, [jobOrderId]);
- const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
- (_e, newValue) => {
- setTabIndex(newValue);
- },
- [],
- );
-
- // 如果选择了 process detail,显示 detail 页面
- if (selectedProcessId !== null) {
- return (
- <ProductionProcessDetail
- jobOrderId={selectedProcessId}
- onBack={() => {
- setSelectedProcessId(null);
- fetchData(); // 刷新数据
- }}
- />
- );
- }
-
- if (loading) {
- return (
- <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
- <CircularProgress/>
- </Box>
- );
- }
-
- if (!processData) {
- return (
- <Box>
- <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
- {t("Back")}
- </Button>
- <Typography sx={{ mt: 2 }}>{t("No data found")}</Typography>
- </Box>
- );
- }
-
-
- // InfoCard 组件内容
- const InfoCardContent = () => (
- <Card sx={{ display: "block", mt: 2 }}>
- <CardContent component={Stack} spacing={4}>
- <Box>
- <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
- <Grid item xs={6}>
- <TextField
- label={t("Job Order Code")}
- fullWidth
- disabled={true}
- value={processData?.jobOrderCode || ""}
- />
- </Grid>
-
- <Grid item xs={6}>
- <TextField
- label={t("Item Code")}
- fullWidth
- disabled={true}
- value={processData?.itemCode+"-"+processData?.itemName || ""}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Job Type")}
- fullWidth
- disabled={true}
- value={t(processData?.jobType) || t("N/A")}
- //value={t("N/A")}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Req. Qty")}
- fullWidth
- disabled={true}
- value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""}
- />
- </Grid>
-
- <Grid item xs={6}>
- <TextField
- value={processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}
- label={t("Target Production Date")}
- fullWidth
- disabled={true}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Production Priority")}
- fullWidth
- disabled={true}
- value={processData?.productionPriority ||processData?.isDense === 0 ? "50" : processData?.productionPriority || "0"}
- />
- </Grid>
- <Grid item xs={6}>
- <TextField
- label={t("Is Dark | Dense | Float| Scrap Rate| Allergic Substance")}
- fullWidth
- disabled={true}
- 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)}`}
- />
- </Grid>
-
- </Grid>
- </Box>
- </CardContent>
- </Card>
- );
-
- const productionProcessesLineRemarkTableColumns: GridColDef[] = [
- {
- field: "seqNo",
- headerName: t("Seq"),
- flex: 0.2,
- align: "left",
- headerAlign: "center",
- type: "number",
- renderCell: (params) => {
- return <Typography sx={{ fontSize: "14px" }}>{params.value}</Typography>;
- },
- },
- {
- field: "description",
- headerName: t("Remark"),
- flex: 1,
- align: "left",
- headerAlign: "center",
- renderCell: (params) => {
- return <Typography sx={{ fontSize: "14px" }}>{params.value || ""}</Typography>;
- },
- },
- ];
- const productionProcessesLineRemarkTableRows =
- processData?.productProcessLines?.map((line: any) => ({
- id: line.seqNo,
- seqNo: line.seqNo,
- description: line.description ?? "",
-
- })) ?? [];
-
-
-
-
- const pickTableColumns: GridColDef[] = [
- {
- field: "id",
- headerName: t("id"),
- flex: 0.2,
- align: "left",
- headerAlign: "left",
- type: "number",
-
- },
- {
- field: "itemCode",
- headerName: t("Item Code"),
- flex: 0.6,
- },
- {
- field: "itemName",
- headerName: t("Item Name"),
- flex: 1,
- renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
- return `${params.value} (${params.row.uom})`;
- },
- },
- {
- field: "reqQty",
- headerName: t("Req. Qty"),
- flex: 0.7,
- align: "right",
- headerAlign: "right",
- renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
- if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
- return t("N/A");
- }
-
- return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`;
- },
- },
- {
- field: "stockAvailable",
- headerName: t("Stock Available"),
- flex: 0.7,
- align: "right",
- headerAlign: "right",
- type: "number",
- renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
- // 如果是 consumables,显示 N/A
- if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
- return t("N/A");
- }
- const stockAvailable = getStockAvailable(params.row);
- if (stockAvailable === null) {
- return t("N/A");
- }
- return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`;
- },
- },
- {
- field: "bomProcessSeqNo",
- headerName: t("Seq No"),
- flex: 0.5,
- align: "right",
- headerAlign: "right",
- type: "number",
- },
- /*
- {
- field: "seqNoRemark",
- headerName: t("Seq No Remark"),
- flex: 1,
- align: "left",
- headerAlign: "left",
- type: "string",
- },
- */
- {
- field: "stockStatus",
- headerName: t("Stock Status"),
- flex: 0.5,
- align: "center",
- headerAlign: "center",
- type: "boolean",
- renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
- if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
- return <Typography>{t("N/A")}</Typography>;
- }
- return isStockSufficient(params.row)
- ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
- : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
- },
- },
- ];
-
- const pickTableRows = jobOrderLines.map((line, index) => ({
- ...line,
- //id: line.id || index,
- id: index + 1,
- }));
-
- const PickTableContent = () => (
- <Box sx={{ mt: 2 }}>
- <ProcessSummaryHeader processData={processData} />
- <Card sx={{ mb: 2 }}>
- <CardContent>
- <Stack
- direction="row"
- alignItems="center"
- justifyContent="space-between"
- spacing={2}
- >
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Total lines: ")}<strong>{stockCounts.total}</strong>
- </Typography>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Lines with sufficient stock: ")}<strong style={{ color: "green" }}>{stockCounts.sufficient}</strong>
- </Typography>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Lines with insufficient stock: ")}<strong style={{ color: "red" }}>{stockCounts.insufficient}</strong>
- </Typography>
- {fromJosave && (
- <Button
- variant="contained"
- color="error"
- onClick={() => handleDeleteJobOrder(jobOrderId)}
- disabled={processData?.jobOrderStatus !== "planning"}
- >
- {t("Delete Job Order")}
- </Button>
- )}
- {fromJosave && (
- <Button
- variant="contained"
- color="primary"
- onClick={() => handleRelease(jobOrderId)}
- disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"}
- >
- {t("Release")}
- </Button>
- )}
-
- </Stack>
- </CardContent>
- </Card>
-
- <StyledDataGrid
- sx={{ "--DataGrid-overlayHeight": "100px" }}
- disableColumnMenu
- rows={pickTableRows}
- columns={pickTableColumns}
- getRowHeight={() => "auto"}
- />
- </Box>
- );
- const ProductionProcessesLineRemarkTableContent = () => (
- <Box sx={{ mt: 2 }}>
- <ProcessSummaryHeader processData={processData} />
- <StyledDataGrid
- sx={{
- "--DataGrid-overlayHeight": "100px",
- }}
- disableColumnMenu
- rows={productionProcessesLineRemarkTableRows ?? []}
- columns={productionProcessesLineRemarkTableColumns}
- getRowHeight={() => 'auto'}
-
- />
- </Box>
- );
-
-
- return (
- <Box>
- {/* 返回按钮 */}
- <Box sx={{ mb: 2 }}>
- <Button variant="outlined" onClick={onBack} startIcon={<ArrowBackIcon />}>
- {t("Back to List")}
- </Button>
- </Box>
-
- {/* 标签页 */}
- <Box sx={{ borderBottom: '1px solid #e0e0e0' }}>
- <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
- <Tab label={t("Job Order Info")} />
- <Tab label={t("BoM Material")} />
-
- <Tab label={t("Production Process")} />
-
-
- <Tab label={t("Production Process Line Remark")} />
- {!fromJosave && (
- <Tab label={t("Matching Stock")} />
- )}
- </Tabs>
- </Box>
-
- {/* 标签页内容 */}
- <Box sx={{ p: 2 }}>
- {tabIndex === 0 && <InfoCardContent />}
- {tabIndex === 1 && <PickTableContent />}
-
- {tabIndex === 2 && (
- <ProductionProcessDetail
- jobOrderId={jobOrderId}
- onBack={() => {
- // 切换回第一个标签页,或者什么都不做
- setTabIndex(0);
-
- }}
- fromJosave={fromJosave}
- />
- )}
- {tabIndex === 3 && <ProductionProcessesLineRemarkTableContent />}
-
- {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />}
-
-
-
- </Box>
- </Box>
- );
- };
-
- export default ProductionProcessJobOrderDetail;
|