Sfoglia il codice sorgente

bag and time

master
CANCERYS\kw093 1 giorno fa
parent
commit
f75796a8db
14 ha cambiato i file con 1449 aggiunte e 501 eliminazioni
  1. +47
    -0
      src/app/api/bag/action.ts
  2. +43
    -0
      src/app/api/jo/actions.ts
  3. +1
    -0
      src/app/api/jo/index.ts
  4. +159
    -84
      src/components/JoSearch/JoCreateFormModal.tsx
  5. +207
    -23
      src/components/JoSearch/JoSearch.tsx
  6. +0
    -26
      src/components/PickOrderSearch/PickExecution.tsx
  7. +309
    -0
      src/components/ProductionProcess/BagConsumptionForm.tsx
  8. +206
    -0
      src/components/ProductionProcess/OverallTimeRemainingCard.tsx
  9. +45
    -10
      src/components/ProductionProcess/ProcessSummaryHeader.tsx
  10. +124
    -219
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  11. +82
    -1
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  12. +23
    -2
      src/components/ProductionProcess/ProductionProcessList.tsx
  13. +179
    -118
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx
  14. +24
    -18
      src/components/Qc/QcComponent.tsx

+ 47
- 0
src/app/api/bag/action.ts Vedi File

@@ -0,0 +1,47 @@
"use server";

import { cache } from 'react';
import { Pageable, serverFetchBlob, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
//import { JobOrder, JoStatus, Machine, Operator } from ".";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil";
import { FileResponse } from "@/app/api/pdf/actions";
export interface GetBagInfoResponse {
id: number;
bagId: number;
bagName: string;
lotId: number;
lotNo: string;
stockOutLineId: number;
code: string;
balanceQty: number;
}
export const getBagInfo = cache(async () => {
return serverFetchJson<GetBagInfoResponse[]>(
`${BASE_API_URL}/bag/bagInfo`,
{
method: "GET",
next: { tags: ["bagInfo"] },
}
);
});

export interface CreateJoBagConsumptionRequest {
bagId: number;
bagLotLineId: number;
jobId: number;
//startQty: number;
consumedQty: number;
scrapQty: number;
}
export const createJoBagConsumption = cache(async (request: CreateJoBagConsumptionRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/bag/createJoBagConsumption`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}
);
});

+ 43
- 0
src/app/api/jo/actions.ts Vedi File

@@ -16,6 +16,7 @@ export interface SaveJo {
type: string;
//jobType?: string;
jobTypeId?: number;
productionPriority?: number;
}

export interface SaveJoResponse {
@@ -246,6 +247,7 @@ export interface ProductProcessWithLinesResponse {
jobOrderId?: number;
jobOrderCode: string;
jobOrderStatus: string;
bomDescription: string;
jobType: string;
isDark: string;
isDense: number;
@@ -321,6 +323,7 @@ export interface AllJoborderProductProcessInfoResponse {
date: string;
matchStatus: string;
bomId?: number;
productionPriority: number;
assignedTo: number;
pickOrderId: number;
pickOrderStatus: string;
@@ -328,6 +331,7 @@ export interface AllJoborderProductProcessInfoResponse {
itemName: string;
requiredQty: number;
jobOrderId: number;
timeNeedToComplete: number;
uom: string;
stockInLineId: number;
jobOrderCode: string;
@@ -578,6 +582,11 @@ export interface JobOrderListForPrintQrCodeResponse {
stockOutLineStatus: string;
finihedTime: string;
}
export interface UpdateJoPlanStartRequest {
id: number;
planStart: string; // Format: YYYY-MM-DDTHH:mm:ss or YYYY-MM-DD
}

export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/issue`,
@@ -1086,4 +1095,38 @@ export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Pr
);

return reportBlob;
};
export const updateJoPlanStart = cache(async (data: UpdateJoPlanStartRequest) => {
return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/update-jo-plan-start`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
})
})

export interface UpdateProductProcessLineStatusRequest {
productProcessLineId: number;
status: string;
}


export const updateProductProcessLineStatus = async (request: UpdateProductProcessLineStatusRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/update/status`,
{
method: "POST",
body: JSON.stringify(request),
headers: { "Content-Type": "application/json" },
}
);
};
export const passProductProcessLine = async (lineId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/pass/${lineId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
}
);
};

+ 1
- 0
src/app/api/jo/index.ts Vedi File

@@ -32,6 +32,7 @@ export interface JobOrder {
jobTypeName: string;
sufficientCount: number;
insufficientCount: number;
productionPriority: number;
// TODO pack below into StockInLineInfo
stockInLineId?: number;
stockInLineStatus?: string;


+ 159
- 84
src/components/JoSearch/JoCreateFormModal.tsx Vedi File

@@ -8,7 +8,7 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { isFinite } from "lodash";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo, useState} from "react";
import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { msg } from "../Swal/CustomAlerts";
@@ -30,17 +30,52 @@ const JoCreateFormModal: React.FC<Props> = ({
onSearch,
}) => {
const { t } = useTranslation("jo");
const [multiplier, setMultiplier] = useState<number>(1);
const formProps = useForm<SaveJo>({
mode: "onChange",
defaultValues: {
productionPriority: 50
}
});
const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps

// 监听 bomId 变化
const selectedBomId = watch("bomId");

/*
const handleAutoCompleteChange = useCallback(
(event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
console.log("BOM changed to:", value);
onChange(value.id);
// 重置倍数为 1
setMultiplier(1);
// 1) 根据 BOM 设置数量(倍数 * outputQty)
if (value.outputQty != null) {
const calculatedQty = 1 * Number(value.outputQty);
formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
}
// 2) 选 BOM 时,把日期默认设为"今天"
const today = dayjs();
const todayStr = dayjsToDateString(today, "input");
formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true });
},
[formProps]
);
*/
// 添加 useEffect 来监听倍数变化,自动计算 reqQty
useEffect(() => {
const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
if (selectedBom && selectedBom.outputQty != null) {
const calculatedQty = multiplier * Number(selectedBom.outputQty);
formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true });
}
}, [multiplier, selectedBomId, bomCombo, formProps]);
const onModalClose = useCallback(() => {
reset()
onClose()
setMultiplier(1);
}, [reset, onClose])

const handleAutoCompleteChange = useCallback(
@@ -61,65 +96,7 @@ const JoCreateFormModal: React.FC<Props> = ({
[formProps]
);

// 使用 useMemo 来计算过滤后的 jobTypes,响应 selectedBomId 变化
/*
const filteredJobTypes = useMemo(() => {
console.log("getFilteredJobTypes called, selectedBomId:", selectedBomId);
if (!selectedBomId) {
console.log("No BOM selected, returning all jobTypes:", jobTypes);
return jobTypes;
}
const selectedBom = bomCombo.find(bom => bom.id === selectedBomId);
console.log("Selected BOM:", selectedBom);
console.log("Selected BOM full object:", JSON.stringify(selectedBom, null, 2));
if (!selectedBom) {
console.log("BOM not found, returning all jobTypes");
return jobTypes;
}
// 检查 description 是否存在
const description = selectedBom.description;
console.log("BOM description (raw):", description);
console.log("BOM description type:", typeof description);
console.log("BOM description is undefined?", description === undefined);
console.log("BOM description is null?", description === null);
if (!description) {
console.log("BOM description is missing or empty, returning all jobTypes");
return jobTypes;
}
const descriptionUpper = description.toUpperCase();
console.log("BOM description (uppercase):", descriptionUpper);
console.log("All jobTypes:", jobTypes);
let filtered: JobTypeResponse[] = [];
if (descriptionUpper === "WIP") {
filtered = jobTypes.filter(jt => {
const jobTypeName = jt.name.toUpperCase();
const shouldInclude = jobTypeName !== "FG";
console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`);
return shouldInclude;
});
} else if (descriptionUpper === "FG") {
filtered = jobTypes.filter(jt => {
const jobTypeName = jt.name.toUpperCase();
const shouldInclude = jobTypeName !== "WIP";
console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`);
return shouldInclude;
});
} else {
filtered = jobTypes;
}
console.log("Filtered jobTypes:", filtered);
return filtered;
}, [bomCombo, jobTypes, selectedBomId]);
*/

// 当 BOM 改变时,自动选择匹配的 Job Type
useEffect(() => {
if (!selectedBomId) {
@@ -174,6 +151,10 @@ const JoCreateFormModal: React.FC<Props> = ({
data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day'))
}
data.jobTypeId = Number(data.jobTypeId);
// 如果 productionPriority 为空或无效,使用默认值 50
data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority)
? Number(data.productionPriority)
: 50;
const response = await manualCreateJo(data)
if (response) {
onSearch();
@@ -283,31 +264,73 @@ const JoCreateFormModal: React.FC<Props> = ({
render={({ field, fieldState: { error } }) => {
const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId"));
const uom = selectedBom?.outputQtyUom || "";
const outputQty = selectedBom?.outputQty ?? 0;
const calculatedValue = multiplier * outputQty;
return (
<TextField
{...field}
label={t("Req. Qty")}
fullWidth
error={Boolean(error)}
variant="outlined"
type="number"
disabled={true}
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value === "" ? undefined : Number(e.target.value);
field.onChange(val);
}}
InputProps={{
endAdornment: uom ? (
<InputAdornment position="end">
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{uom}
</Typography>
</InputAdornment>
) : null
}}
/>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<TextField
label={t("Base Qty")}
fullWidth
type="number"
variant="outlined"
value={outputQty}
disabled
InputProps={{
endAdornment: uom ? (
<InputAdornment position="end">
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{uom}
</Typography>
</InputAdornment>
) : null
}}
sx={{ flex: 1 }}
/>

<Typography variant="body1" sx={{ color: "text.secondary" }}>
×
</Typography>
<TextField
label={t("Batch Count")}
fullWidth
type="number"
variant="outlined"
value={multiplier}
onChange={(e) => {
const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
setMultiplier(val);
}}
inputProps={{
min: 1,
step: 1
}}
sx={{ flex: 1 }}
/>
<Typography variant="body1" sx={{ color: "text.secondary" }}>
=
</Typography>
<TextField
{...field}
label={t("Req. Qty")}
fullWidth
error={Boolean(error)}
variant="outlined"
type="number"
value={calculatedValue || ""}
disabled
InputProps={{
endAdornment: uom ? (
<InputAdornment position="end">
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{uom}
</Typography>
</InputAdornment>
) : null
}}
sx={{ flex: 1 }}
/>
</Box>
);
}}
/>
@@ -349,6 +372,58 @@ const JoCreateFormModal: React.FC<Props> = ({
}}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}
name="productionPriority"
rules={{
required: t("Production Priority required!") as string,
max: {
value: 100,
message: t("Production Priority cannot exceed 100") as string
},
min: {
value: 1,
message: t("Production Priority must be at least 1") as string
},
validate: (value) => {
if (value === undefined || value === null || isNaN(value)) {
return t("Production Priority required!") as string;
}
return true;
}
}}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label={t("Production Priority")}
fullWidth
error={Boolean(error)}
variant="outlined"
type="number"
inputProps={{
min: 1,
max: 100,
step: 1
}}
value={field.value ?? ""}
onChange={(e) => {
const inputValue = e.target.value;
// 允许空字符串(用户正在删除)
if (inputValue === "") {
field.onChange("");
return;
}
// 转换为数字并验证范围
const numValue = Number(inputValue);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 100) {
field.onChange(numValue);
}
}}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}


+ 207
- 23
src/components/JoSearch/JoSearch.tsx Vedi File

@@ -1,5 +1,5 @@
"use client"
import { SearchJoResultRequest, fetchJos, updateJo } from "@/app/api/jo/actions";
import { SearchJoResultRequest, fetchJos, updateJo,updateProductProcessPriority } from "@/app/api/jo/actions";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Criterion } from "../SearchBox";
@@ -12,10 +12,11 @@ import { useRouter } from "next/navigation";
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { StockInLineInput } from "@/app/api/stockIn";
import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo";
import { Button, Stack } from "@mui/material";
import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment } from "@mui/material";
import { BomCombo } from "@/app/api/bom";
import JoCreateFormModal from "./JoCreateFormModal";
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import QcStockInModal from "../Qc/QcStockInModal";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
@@ -26,6 +27,10 @@ import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { updateJoPlanStart } from "@/app/api/jo/actions";
import { arrayToDayjs } from "@/app/utils/formatUtil";

interface Props {
defaultInputs: SearchJoResultRequest,
@@ -50,7 +55,14 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());

const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false);
const [operationPriority, setOperationPriority] = useState<number>(50);
const [selectedJo, setSelectedJo] = useState<JobOrder | null>(null);
const [selectedProductProcessId, setSelectedProductProcessId] = useState<number | null>(null);
const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false);
const [planStartDate, setPlanStartDate] = useState<dayjs.Dayjs | null>(null);
const [selectedJoForDate, setSelectedJoForDate] = useState<JobOrder | null>(null);
const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
const response = await fetch(`/api/jo/detail?id=${id}`);
if (!response.ok) {
@@ -98,6 +110,32 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

fetchInventoryData();
}, []);
const handleOpenPriorityDialog = useCallback(async (jo: JobOrder) => {
setSelectedJo(jo);
setOperationPriority(jo.productionPriority ?? 50);
// 获取 productProcessId
try {
const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions");
const processes = await fetchProductProcessesByJobOrderId(jo.id);
if (processes && processes.length > 0) {
setSelectedProductProcessId(processes[0].id);
setOpenOperationPriorityDialog(true);
} else {
msg(t("No product process found for this job order"));
}
} catch (error) {
console.error("Error fetching product process:", error);
msg(t("Error loading product process"));
}
}, [t]);
const handleClosePriorityDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
setOpenOperationPriorityDialog(false);
setSelectedJo(null);
setSelectedProductProcessId(null);
}, []);

const getStockAvailable = (pickLine: JoDetailPickLine) => {
const inventory = inventoryData.find(inventory =>
@@ -137,14 +175,71 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
options: jobTypes.map(jt => jt.name)
},
], [t, jobTypes])

const handleOpenPlanStartDialog = useCallback((jo: JobOrder) => {
setSelectedJoForDate(jo);
// 将 planStart 数组转换为 dayjs 对象
if (jo.planStart && Array.isArray(jo.planStart)) {
setPlanStartDate(arrayToDayjs(jo.planStart));
} else {
setPlanStartDate(dayjs());
}
setOpenPlanStartDialog(true);
}, []);
const columns = useMemo<Column<JobOrder>[]>(
() => [
{
name: "planStart",
label: t("Estimated Production Date"),
align: "left",
headerAlign: "left",
renderCell: (row) => {
return (
<Stack direction="row" alignItems="center" spacing={1}>
<span>{row.planStart ? arrayToDateString(row.planStart) : '-'}</span>
{row.status == "planning" && (
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleOpenPlanStartDialog(row);
}}
sx={{ padding: '4px' }}
>
<EditIcon fontSize="small" />
</IconButton>
)}
</Stack>
);
}
},
{
name: "productionPriority",
label: t("Production Priority"),
renderCell: (row) => {
return (
<Stack direction="row" alignItems="center" spacing={1}>
<span>{integerFormatter.format(row.productionPriority)}</span>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleOpenPriorityDialog(row);
}}
sx={{ padding: '4px' }}
>
<EditIcon fontSize="small" />
</IconButton>
</Stack>
);
}
},
{
name: "code",
label: t("Code"),
flex: 2
},

{
name: "item",
label: `${t("Item Name")}`,
@@ -170,23 +265,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
}
},
{
name: "status",
label: t("Status"),
renderCell: (row) => {
return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
{t(upperFirst(row.status))}
</span>
}
},{
name: "planStart",
label: t("Estimated Production Date"),
align: "left",
headerAlign: "left",
renderCell: (row) => {
return row.planStart ? arrayToDateString(row.planStart) : '-'
}
},
{
name: "stockStatus" as keyof JobOrder,
label: t("BOM Status"),
@@ -201,6 +279,15 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
);
}
},
{
name: "status",
label: t("Status"),
renderCell: (row) => {
return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
{t(upperFirst(row.status))}
</span>
}
},
{
name: "jobTypeName",
label: t("Job Type"),
@@ -226,7 +313,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
)
}
},
], [t, inventoryData, detailedJos]
], [t, inventoryData, detailedJos, handleOpenPriorityDialog,handleOpenPlanStartDialog]
)

// 按照 PoSearch 的模式:创建 newPageFetch 函数
@@ -256,7 +343,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
},
[],
);

const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
const response = await updateProductProcessPriority(productProcessId, productionPriority)
if (response) {
// 刷新数据
await newPageFetch(pagingController, inputs);
}
}, [pagingController, inputs, newPageFetch]);
const handleConfirmPriority = useCallback(async () => {
if (!selectedProductProcessId) return;
await handleUpdateOperationPriority(selectedProductProcessId, Number(operationPriority));
setOpenOperationPriorityDialog(false);
setSelectedJo(null);
setSelectedProductProcessId(null);
}, [selectedProductProcessId, operationPriority, handleUpdateOperationPriority]);
// 按照 PoSearch 的模式:使用相同的 useEffect 逻辑
useEffect(() => {
newPageFetch(pagingController, inputs);
@@ -352,6 +452,31 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
setPagingController(defaultPagingController);
}, [defaultInputs])

const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
setOpenPlanStartDialog(false);
setSelectedJoForDate(null);
setPlanStartDate(null);
}, []);
const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
const response = await updateJoPlanStart({ id: jobOrderId, planStart });
if (response) {
// 刷新数据
await newPageFetch(pagingController, inputs);
}
}, [pagingController, inputs, newPageFetch]);
const handleConfirmPlanStart = useCallback(async () => {
if (!selectedJoForDate?.id || !planStartDate) return;
// 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
await handleUpdatePlanStart(selectedJoForDate.id, dateString);
setOpenPlanStartDialog(false);
setSelectedJoForDate(null);
setPlanStartDate(null);
}, [selectedJoForDate, planStartDate, handleUpdatePlanStart]);

const onReset = useCallback(() => {
setInputs(defaultInputs);
@@ -413,7 +538,66 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
inputDetail={modalInfo}
printerCombo={printerCombo}
/>
<Dialog
open={openOperationPriorityDialog}
onClose={handleClosePriorityDialog}
fullWidth
maxWidth="xs"
>
<DialogTitle>{t("Update Production Priority")}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label={t("Production Priority")}
type="number"
fullWidth
value={operationPriority}
onChange={(e) => setOperationPriority(Number(e.target.value))}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
<Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
</DialogActions>
</Dialog>
<Dialog
open={openPlanStartDialog}
onClose={handleClosePlanStartDialog}
fullWidth
maxWidth="xs"
>
<DialogTitle>{t("Update Estimated Production Date")}</DialogTitle>
<DialogContent>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={t("Estimated Production Date")}
value={planStartDate}
onChange={(newValue) => setPlanStartDate(newValue)}
slotProps={{
textField: {
fullWidth: true,
margin: "dense",
autoFocus: true,
}
}}
/>
</LocalizationProvider>
</DialogContent>
<DialogActions>
<Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleConfirmPlanStart}
disabled={!planStartDate}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</>


}

export default JoSearch;

+ 0
- 26
src/components/PickOrderSearch/PickExecution.tsx Vedi File

@@ -203,32 +203,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs);
}, [fetchNewPageConsoPickOrder, filterArgs]);

const handleUpdateStockOutLineStatus = useCallback(async (
stockOutLineId: number,
status: string,
qty?: number
) => {
try {
const updateData = {
id: stockOutLineId,
status: status,
qty: qty
};
console.log("Updating stock out line status:", updateData);
const result = await updateStockOutLineStatus(updateData);
if (result) {
console.log("Stock out line status updated successfully:", result);
if (selectedRowId) {
handleRowSelect(selectedRowId);
}
}
} catch (error) {
console.error("Error updating stock out line status:", error);
}
}, [selectedRowId]);

const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => {
let isReleasable = true;


+ 309
- 0
src/components/ProductionProcess/BagConsumptionForm.tsx Vedi File

@@ -0,0 +1,309 @@
"use client";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Box,
Paper,
Typography,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Select,
MenuItem,
Button,
IconButton,
CircularProgress,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import { useTranslation } from "react-i18next";
import {
getBagInfo,
createJoBagConsumption,
GetBagInfoResponse,
CreateJoBagConsumptionRequest,
} from "@/app/api/bag/action";
import { fetchProductProcessLineDetail } from "@/app/api/jo/actions";

export interface BagConsumptionRow {
bagId: number;
bagLotLineId: number;
consumedQty: number;
scrapQty: number;
}

interface BagConsumptionFormProps {
jobOrderId: number;
lineId: number;
bomDescription?: string;
isLastLine: boolean;
onRefresh?: () => void;
}

const BagConsumptionForm: React.FC<BagConsumptionFormProps> = ({
jobOrderId,
lineId,
bomDescription,
isLastLine,
onRefresh,
}) => {
const { t } = useTranslation(["common", "jo"]);
const [bagList, setBagList] = useState<GetBagInfoResponse[]>([]);
const [bagConsumptionRows, setBagConsumptionRows] = useState<BagConsumptionRow[]>([
{ bagId: 0, bagLotLineId: 0, consumedQty: 0, scrapQty: 0 },
]);
const [isLoadingBags, setIsLoadingBags] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);

// 判断是否显示表单
const shouldShow = useMemo(() => {
return bomDescription === "FG" && isLastLine;
}, [bomDescription, isLastLine]);

// 加载 Bag 列表
useEffect(() => {
if (shouldShow) {
setIsLoadingBags(true);
getBagInfo()
.then((bags) => {
setBagList(bags);
console.log("✅ Bag list loaded:", bags);
})
.catch((error) => {
console.error("❌ Error loading bag list:", error);
})
.finally(() => {
setIsLoadingBags(false);
});
}
}, [shouldShow]);

// 添加 Bag 行
const handleAddBagRow = useCallback(() => {
setBagConsumptionRows((prev) => [
...prev,
{ bagId: 0, bagLotLineId: 0, consumedQty: 0, scrapQty: 0 },
]);
}, []);

// 删除 Bag 行
const handleDeleteBagRow = useCallback((index: number) => {
setBagConsumptionRows((prev) => prev.filter((_, i) => i !== index));
}, []);

// 更新 Bag 行数据
const handleBagRowChange = useCallback(
(index: number, field: keyof BagConsumptionRow, value: any) => {
setBagConsumptionRows((prev) =>
prev.map((row, i) => (i === index ? { ...row, [field]: value } : row))
);
},
[]
);

// 当选择 bag 时,自动填充 bagLotLineId
const handleBagSelect = useCallback(
(index: number, bagLotLineId: number) => {
const selectedBag = bagList.find((b) => b.id === bagLotLineId);
if (selectedBag) {
handleBagRowChange(index, "bagId", selectedBag.bagId);
handleBagRowChange(index, "bagLotLineId", selectedBag.id);
}
},
[bagList, handleBagRowChange]
);

// 提交 Bag Consumption
const handleSubmitBagConsumption = useCallback(async () => {
if (!jobOrderId || !lineId) {
alert(t("Missing job order ID or line ID"));
return;
}

try {
setIsSubmitting(true);

// 过滤掉未选择 bag 的行
const validRows = bagConsumptionRows.filter(
(row) => row.bagId > 0 && row.bagLotLineId > 0
);

if (validRows.length === 0) {
alert(t("Please select at least one bag"));
return;
}

// 提交每个 bag consumption
const promises = validRows.map((row) => {
const selectedBag = bagList.find((b) => b.id === row.bagLotLineId);
const request: CreateJoBagConsumptionRequest = {
bagId: row.bagId,
bagLotLineId: row.bagLotLineId,
jobId: jobOrderId,
//startQty: selectedBag?.balanceQty || 0,
consumedQty: row.consumedQty,
scrapQty: row.scrapQty,
};
return createJoBagConsumption(request);
});

await Promise.all(promises);
console.log("✅ Bag consumption submitted successfully");

// 清空表单
setBagConsumptionRows([
{ bagId: 0, bagLotLineId: 0, consumedQty: 0, scrapQty: 0 },
]);

// 刷新 line detail
if (onRefresh) {
onRefresh();
} else {
const detail = await fetchProductProcessLineDetail(lineId);
console.log("✅ Line detail refreshed:", detail);
}
} catch (error: any) {
console.error("❌ Error submitting bag consumption:", error);
// ✅ 显示更详细的错误信息
const errorMessage = error?.message ||
error?.response?.data?.message ||
t("Failed to submit bag consumption. Please try again.");
alert(errorMessage);
} finally {
setIsSubmitting(false);
}
}, [bagConsumptionRows, bagList, jobOrderId, lineId, onRefresh, t]);

// 如果不满足显示条件,不渲染
if (!shouldShow) {
return null;
}

return (
<Box sx={{ mt: 3 }}>
<Paper sx={{ p: 3, bgcolor: "info.50" }}>
<Typography variant="h6" gutterBottom>
{t("Bag Consumption")}
</Typography>

{isLoadingBags ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<Table size="small" sx={{ mt: 2 }}>
<TableHead>
<TableRow>
<TableCell width="40%">{t("Bag")}</TableCell>
<TableCell width="25%" align="right">
{t("Consumed Qty")}
</TableCell>
<TableCell width="25%" align="right">
{t("Scrap Qty")}
</TableCell>
<TableCell width="10%" align="center">
{t("Action")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{bagConsumptionRows.map((row, index) => (
<TableRow key={index}>
<TableCell>
<Select
fullWidth
size="small"
value={row.bagLotLineId || 0} // ✅ 改为使用 bagLotLineId
onChange={(e) =>
handleBagSelect(index, Number(e.target.value))
}
displayEmpty
>
<MenuItem value={0}>
<em>{t("Select Bag")}</em>
</MenuItem>
{bagList.map((bag) => (
<MenuItem key={bag.id} value={bag.id}> {/* ✅ 改为使用 bag.id (bagLotLineId) */}
{bag.bagName} ({bag.code}) - {t("Balance")}:{" "}
{bag.balanceQty}
</MenuItem>
))}
</Select>
</TableCell>
<TableCell align="right">
<TextField
type="number"
size="small"
fullWidth
value={row.consumedQty}
onChange={(e) =>
handleBagRowChange(
index,
"consumedQty",
Number(e.target.value) || 0
)
}
inputProps={{ min: 0 }}
/>
</TableCell>
<TableCell align="right">
<TextField
type="number"
size="small"
fullWidth
value={row.scrapQty}
onChange={(e) =>
handleBagRowChange(
index,
"scrapQty",
Number(e.target.value) || 0
)
}
inputProps={{ min: 0 }}
/>
</TableCell>
<TableCell align="center">
{bagConsumptionRows.length > 1 && (
<IconButton
size="small"
color="error"
onClick={() => handleDeleteBagRow(index)}
>
<DeleteIcon />
</IconButton>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>

<Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button
variant="outlined"
startIcon={<AddIcon />}
onClick={handleAddBagRow}
>
{t("Select Another Bag Lot")}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleSubmitBagConsumption}
disabled={isSubmitting}
>
{isSubmitting ? t("Submitting...") : t("Submit Bag Consumption")}
</Button>
</Box>
</>
)}
</Paper>
</Box>
);
};

export default BagConsumptionForm;

+ 206
- 0
src/components/ProductionProcess/OverallTimeRemainingCard.tsx Vedi File

@@ -0,0 +1,206 @@
"use client";
import React, { useEffect, useState } from "react";
import { Card, CardContent, Typography, Box } from "@mui/material";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import { ProductProcessWithLinesResponse } from "@/app/api/jo/actions";

interface OverallTimeRemainingCardProps {
processData?: ProductProcessWithLinesResponse | null;
}

const OverallTimeRemainingCard: React.FC<OverallTimeRemainingCardProps> = ({
processData,
}) => {
const { t } = useTranslation(["common", "jo"]);
const [overallRemainingTime, setOverallRemainingTime] = useState<string | null>(null);
const [isOverTime, setIsOverTime] = useState(false);

useEffect(() => {
console.log("🕐 OverallTimeRemainingCard - processData:", processData);
console.log("🕐 OverallTimeRemainingCard - processData?.startTime:", processData?.startTime);
console.log("🕐 OverallTimeRemainingCard - processData?.startTime type:", typeof processData?.startTime);
console.log("🕐 OverallTimeRemainingCard - processData?.startTime isArray:", Array.isArray(processData?.startTime));
if (!processData?.startTime) {
console.log("❌ OverallTimeRemainingCard - No startTime found");
setOverallRemainingTime(null);
setIsOverTime(false);
return;
}

// 计算 Assume Time Need:从所有 productProcessLines 的 durationInMinutes 求和
const assumeTimeNeed = processData?.productProcessLines?.reduce(
(sum, line) => sum + (line.durationInMinutes || 0),
0
) || 0;

console.log("🕐 OverallTimeRemainingCard - productProcessLines:", processData?.productProcessLines);
console.log("🕐 OverallTimeRemainingCard - assumeTimeNeed (minutes):", assumeTimeNeed);

if (assumeTimeNeed === 0) {
console.log("❌ OverallTimeRemainingCard - assumeTimeNeed is 0");
setOverallRemainingTime(null);
setIsOverTime(false);
return;
}

// ✅ 修复:正确处理 startTime 可能是数组的情况
let start: dayjs.Dayjs;
if (Array.isArray(processData.startTime)) {
console.log("🕐 OverallTimeRemainingCard - startTime is array:", processData.startTime);
const [year, month, day, hour = 0, minute = 0, second = 0] = processData.startTime;
console.log("🕐 OverallTimeRemainingCard - Parsed array values:", { year, month, day, hour, minute, second });
start = dayjs(new Date(year, month - 1, day, hour, minute, second));
} else if (typeof processData.startTime === 'string') {
console.log("🕐 OverallTimeRemainingCard - startTime is string:", processData.startTime);
// ✅ 检查是否是 "MM-DD HH:mm" 格式(缺少年份)
const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/;
const match = processData.startTime.match(mmddHhmmPattern);
if (match) {
console.log("🕐 OverallTimeRemainingCard - Detected MM-DD HH:mm format");
const month = parseInt(match[1], 10);
const day = parseInt(match[2], 10);
const hour = parseInt(match[3], 10);
const minute = parseInt(match[4], 10);
// ✅ 使用当前年份,但如果跨年(startTime 是年末,当前是年初),使用上一年
const now = dayjs();
let year = now.year();
// 检查是否跨年:如果 startTime 的月份大于当前月份,或者月份相同但日期大于当前日期
// 且当前日期是年初(1月前10天),则可能是跨年情况
const startMonthDay = month * 100 + day; // 例如 1229 = 12月29日
const nowMonthDay = now.month() * 100 + now.date(); // 例如 101 = 1月1日
// 如果 startTime 是年末(12月且日期>=20),当前是年初(1月且日期<=10),则使用上一年
if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) {
year = now.year() - 1;
console.log("🕐 OverallTimeRemainingCard - Detected year crossover, using previous year:", year);
} else {
console.log("🕐 OverallTimeRemainingCard - Using current year:", year);
}
console.log("🕐 OverallTimeRemainingCard - Parsed MM-DD HH:mm values:", { year, month, day, hour, minute });
start = dayjs(new Date(year, month - 1, day, hour, minute, 0));
} else {
// 尝试直接解析(可能是其他格式)
console.log("🕐 OverallTimeRemainingCard - Trying to parse as standard date string");
start = dayjs(processData.startTime);
}
} else {
console.log("🕐 OverallTimeRemainingCard - startTime is unknown type, trying dayjs");
start = dayjs(processData.startTime as any);
}
console.log("🕐 OverallTimeRemainingCard - start (dayjs):", start.format("YYYY-MM-DD HH:mm:ss"));
console.log("🕐 OverallTimeRemainingCard - start isValid:", start.isValid());
console.log("🕐 OverallTimeRemainingCard - start timestamp:", start.valueOf());

if (!start.isValid()) {
console.error("❌ OverallTimeRemainingCard - Invalid startTime:", processData.startTime);
setOverallRemainingTime(null);
setIsOverTime(false);
return;
}

const assumeEnd = start.add(assumeTimeNeed, 'minute');
console.log("🕐 OverallTimeRemainingCard - assumeEnd (dayjs):", assumeEnd.format("YYYY-MM-DD HH:mm:ss"));
console.log("🕐 OverallTimeRemainingCard - assumeEnd timestamp:", assumeEnd.valueOf());
console.log("🕐 OverallTimeRemainingCard - Duration from start to end (minutes):", assumeTimeNeed);

const update = () => {
const now = dayjs();
const remaining = assumeEnd.diff(now, 'millisecond');

console.log("🕐 OverallTimeRemainingCard - update() called:");
console.log(" - now:", now.format("YYYY-MM-DD HH:mm:ss"));
console.log(" - now timestamp:", now.valueOf());
console.log(" - assumeEnd:", assumeEnd.format("YYYY-MM-DD HH:mm:ss"));
console.log(" - assumeEnd timestamp:", assumeEnd.valueOf());
console.log(" - remaining (ms):", remaining);
console.log(" - remaining (minutes):", remaining / 60000);
console.log(" - remaining (hours):", remaining / 3600000);

if (remaining <= 0) {
const overTime = Math.abs(remaining);
console.log(" - overTime (ms):", overTime);
console.log(" - overTime (minutes):", overTime / 60000);
console.log(" - overTime (hours):", overTime / 3600000);
// ✅ 修复:正确格式化时间,显示小时:分钟:秒
const hours = Math.floor(overTime / 3600000);
const minutes = Math.floor((overTime % 3600000) / 60000);
const seconds = Math.floor((overTime % 60000) / 1000);
console.log(" - Calculated hours:", hours);
console.log(" - Calculated minutes:", minutes);
console.log(" - Calculated seconds:", seconds);
// 如果超过1小时,显示 HH:MM:SS,否则显示 MM:SS
if (hours > 0) {
const formatted = `-${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
console.log(" - Formatted time (with hours):", formatted);
setOverallRemainingTime(formatted);
} else {
const formatted = `-${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
console.log(" - Formatted time (no hours):", formatted);
setOverallRemainingTime(formatted);
}
setIsOverTime(true);
} else {
// ✅ 修复:正确格式化时间,显示小时:分钟:秒
const hours = Math.floor(remaining / 3600000);
const minutes = Math.floor((remaining % 3600000) / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
console.log(" - Calculated hours:", hours);
console.log(" - Calculated minutes:", minutes);
console.log(" - Calculated seconds:", seconds);
// 如果超过1小时,显示 HH:MM:SS,否则显示 MM:SS
if (hours > 0) {
const formatted = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
console.log(" - Formatted time (with hours):", formatted);
setOverallRemainingTime(formatted);
} else {
const formatted = `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
console.log(" - Formatted time (no hours):", formatted);
setOverallRemainingTime(formatted);
}
setIsOverTime(false);
}
};

update();
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}, [processData?.startTime, processData?.productProcessLines]);

if (!processData?.startTime || overallRemainingTime === null) {
return null;
}

return (
<Card sx={{ bgcolor: isOverTime ? 'error.50' : 'info.50', border: '2px solid', borderColor: isOverTime ? 'error.main' : 'info.main', mb: 3 }}>
<CardContent>
<Typography variant="h6" color={isOverTime ? 'error.main' : 'info.main'} gutterBottom fontWeight="bold">
{t("Overall Time Remaining")}
</Typography>
<Box sx={{ mt: 2, p: 2, bgcolor: isOverTime ? 'error.100' : 'info.100', borderRadius: 1 }}>
<Typography
variant="h4"
fontWeight="bold"
color={isOverTime ? 'error.main' : 'info.main'}
align="center"
>
{isOverTime ? `${t("Over Time")}: ${overallRemainingTime}` : overallRemainingTime}
</Typography>
</Box>
</CardContent>
</Card>
);
};

export default OverallTimeRemainingCard;

+ 45
- 10
src/components/ProductionProcess/ProcessSummaryHeader.tsx Vedi File

@@ -2,21 +2,29 @@ import { Card, CardContent, Stack, Typography } from "@mui/material";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import { useTranslation } from "react-i18next";
import { ProductProcessWithLinesResponse } from "@/app/api/jo/actions";

interface Props {
processData?: {
jobOrderCode?: string;
itemCode?: string;
itemName?: string;
jobType?: string;
outputQty?: number | string;
outputQtyUom?: string;
date?: string;
};
processData?: ProductProcessWithLinesResponse | null;
}

const ProcessSummaryHeader: React.FC<Props> = ({ processData }) => {
const { t } = useTranslation();
const { t } = useTranslation(["common", "jo"]);
// 计算 Assume Time Need:从所有 productProcessLines 的 durationInMinutes 求和
const assumeTimeNeed = processData?.productProcessLines?.reduce(
(sum, line) => sum + (line.durationInMinutes || 0),
0
) || 0;
// Start Time:使用 processData.startTime
const startTime = processData?.startTime;
// Assume End Time:Start Time + Assume Time Need(分钟)
const assumeEndTime = startTime
? dayjs(startTime).add(assumeTimeNeed, 'minute')
: null;
return (
<Card sx={{ mb: 2 }}>
<CardContent>
@@ -38,6 +46,33 @@ const ProcessSummaryHeader: React.FC<Props> = ({ processData }) => {
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Production Date")}: <strong style={{ color: "green" }}>{processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}</strong>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Assume Time Need")}: <strong style={{ color: "blue" }}>{assumeTimeNeed} {t("minutes")}</strong>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Start Time")}: <strong>
{startTime
? (
<>
<span style={{ color: "green" }}>{dayjs(startTime).format("MM-DD")}</span>
{" "}
<span style={{ color: "blue" }}>{dayjs(startTime).format("HH:mm")}</span>
</>
)
: "-"}
</strong>
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Assume End Time")}: <strong>
{assumeEndTime ? (
<>
<span style={{ color: "green" }}>{assumeEndTime.format("MM-DD")}</span>
{" "}
<span style={{ color: "blue" }}>{assumeEndTime.format("HH:mm")}</span>
</>
) : "-"}
</strong>
</Typography>
</Stack>
</CardContent>
</Card>


+ 124
- 219
src/components/ProductionProcess/ProductionProcessDetail.tsx Vedi File

@@ -39,8 +39,13 @@ import {
JobOrderProcessLineDetailResponse,
ProductProcessLineInfoResponse,
startProductProcessLine,
fetchProductProcessesByJobOrderId
fetchProductProcessesByJobOrderId,
ProductProcessWithLinesResponse, // ✅ 添加
ProductProcessLineResponse,
passProductProcessLine,
} from "@/app/api/jo/actions";
import { updateProductProcessLineStatus } from "@/app/api/jo/actions";

import { fetchNameList, NameList } from "@/app/api/user/actions";
import ProductionProcessStepExecution from "./ProductionProcessStepExecution";
import ProductionOutputFormPage from "./ProductionOutputFormPage";
@@ -62,8 +67,8 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [showOutputPage, setShowOutputPage] = useState(false);
// 基本信息
const [processData, setProcessData] = useState<any>(null);
const [lines, setLines] = useState<ProductProcessLineInfoResponse[]>([]);
const [processData, setProcessData] = useState<ProductProcessWithLinesResponse | null>(null); // ✅ 修改类型
const [lines, setLines] = useState<ProductProcessLineResponse[]>([]); // ✅ 修改类型
const [loading, setLoading] = useState(false);
// 选中的 line 和执行状态
@@ -125,7 +130,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
setProcessData(currentProcess);
// 使用 productProcessLines 字段(API 返回的字段名)
const lines = (currentProcess as any).productProcessLines || [];
const lines = currentProcess.productProcessLines || [];
setLines(lines);
console.log(" Process data loaded:", currentProcess);
@@ -143,105 +148,27 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
fetchProcessDetail();
}, [fetchProcessDetail]);

// 开始执行某个 line

// 提交产出数据
/*
const processQrCode = useCallback((qrValue: string, lineId: number) => {
// 操作员格式:{2fitestu1} - 键盘模拟输入(测试用)
if (qrValue.match(/\{2fitestu(\d+)\}/)) {
const match = qrValue.match(/\{2fitestu(\d+)\}/);
const userId = parseInt(match![1]);
fetchNameList().then((users: NameList[]) => {
const user = users.find((u: NameList) => u.id === userId);
if (user) {
setScannedOperatorId(user.id);
}
});
return;
}
// 设备格式:{2fiteste1} - 键盘模拟输入(测试用)
if (qrValue.match(/\{2fiteste(\d+)\}/)) {
const match = qrValue.match(/\{2fiteste(\d+)\}/);
const equipmentId = parseInt(match![1]);
setScannedEquipmentId(equipmentId);
return;
}

// 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1"
const trimmedValue = qrValue.trim();
// 检查 operatorId 格式
const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i);
if (operatorMatch) {
const operatorId = parseInt(operatorMatch[1]);
fetchNameList().then((users: NameList[]) => {
const user = users.find((u: NameList) => u.id === operatorId);
if (user) {
setScannedOperatorId(user.id);
} else {
console.warn(`User with ID ${operatorId} not found`);
}
});
return;
}
// 检查 equipmentId 格式
const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i);
if (equipmentMatch) {
const equipmentId = parseInt(equipmentMatch[1]);
setScannedEquipmentId(equipmentId);
return;
}
// 其他格式处理(JSON、普通文本等)
const handlePassLine = useCallback(async (lineId: number) => {
try {
const qrData = JSON.parse(qrValue);
// TODO: 处理 JSON 格式的 QR 码
} catch {
// 普通文本格式
// TODO: 处理普通文本格式
await passProductProcessLine(lineId);
// 刷新数据
await fetchProcessDetail();
} catch (error) {
console.error("Error passing line:", error);
alert(t("Failed to pass line. Please try again."));
}
}, []);
*/
}, [fetchProcessDetail, t]);

// 提交产出数据
const processQrCode = useCallback((qrValue: string, lineId: number) => {
// 设备快捷格式:{2fiteste数字} - 自动生成 equipmentTypeSubTypeEquipmentNo
// 格式:{2fiteste数字} = line.equipment_name + "-数字號"
// 例如:{2fiteste1} = "包裝機類-真空八爪魚機-1號"
if (qrValue.match(/\{2fiteste(\d+)\}/)) {
const match = qrValue.match(/\{2fiteste(\d+)\}/);
const equipmentNo = parseInt(match![1]);
// 根据 lineId 找到对应的 line
const currentLine = lines.find(l => l.id === lineId);
if (currentLine && currentLine.equipment_name) {
const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`;
setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
// 如果找不到 line,尝试从 API 获取 line detail
console.warn(`Line with ID ${lineId} not found in current lines, fetching from API...`);
fetchProductProcessLineDetail(lineId)
.then((lineDetail) => {
// 从 lineDetail 中获取 equipment_name
const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || "";
if (equipmentName) {
const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`;
setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo);
console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`);
} else {
console.warn(`Equipment name not found in line detail for lineId: ${lineId}`);
}
})
.catch((err) => {
console.error(`Failed to fetch line detail for lineId ${lineId}:`, err);
});
}
// 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码
// 格式:{2fitesteXXX} = equipmentCode: "XXX"
// 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號"
if (qrValue.match(/\{2fiteste(.+)\}/)) {
const match = qrValue.match(/\{2fiteste(.+)\}/);
const equipmentCode = match![1];
setScannedEquipmentCode(equipmentCode);
console.log(`Set equipmentCode from shortcut: ${equipmentCode}`);
return;
}
@@ -334,7 +261,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
const effectiveEquipmentCode =
scannedEquipmentCode ?? null;
console.log("Submitting scan data with equipmentCode:", {
productProcessLineId: lineId,
staffNo: scannedStaffNo,
@@ -538,53 +465,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {

return (
<Box>
{/*
<Box sx={{ mb: 2 }}>
<Button variant="outlined" onClick={onBack}>
{t("Back to List")}
</Button>
</Box>


<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold">
{t("Production Process Information")}
</Typography>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Job Order Code")}:</strong> {processData?.jobOrderCode}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Is Dark")}:</strong> {processData?.isDark}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Is Dense")}:</strong> {processData?.isDense}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Is Float")}:</strong> {processData?.isFloat}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Output Qty")}:</strong> {processData?.outputQty+" "+"("+processData?.outputQtyUom +")"}
</Typography>
<Box>
<strong>{t("Status")}:</strong>{" "}
<Chip
label={
processData?.status === 'completed' ? t("Completed") : processData?.status === 'IN_PROGRESS' ? t("In Progress") : processData?.status === 'pending' ? t("Pending") : t("Unknown")
}
color={processData?.status === 'completed' ? 'success' : processData?.status === 'IN_PROGRESS' ? 'success' : processData?.status === 'pending' ? 'primary' : 'error'}
size="small"
/>
</Box>
<Typography variant="subtitle1">
<strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Total Steps")}:</strong> {lines.length}
</Typography>
</Stack>
</Paper>
*/}
{/* ========== 第二部分:Process Lines ========== */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold">
@@ -602,25 +482,12 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<TableCell>{t("Description")}</TableCell>
<TableCell>{t("EquipmentType-EquipmentName-Code")}</TableCell>
<TableCell>{t("Operator")}</TableCell>
{/*}
<TableCell>{t("Processing Time (mins)")}</TableCell>
<TableCell>{t("Setup Time (mins)")}</TableCell>
<TableCell>{t("Changeover Time (mins)")}</TableCell>
*/}

<TableCell>{t("Assume End Time")}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Time Information(mins)")}
</Typography>
{/*
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{t("Processing Time")}-
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{t("Setup Time")} - {t("Changeover Time")}
</Typography>
*/}
</Box>
</TableCell>
<TableCell align="center">{t("Status")}</TableCell>
@@ -638,7 +505,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress';
const isPaused = statusLower === 'paused';
const isPending = statusLower === 'pending' || status === '';
const isPass = statusLower === 'pass';
const isPassDisabled = isCompleted || isPass;
return (
<TableRow key={line.id}>
<TableCell>{line.seqNo}</TableCell>
@@ -648,12 +516,16 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<TableCell><Typography fontWeight={500}>{line.description || "-"}</Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.operatorName}</Typography></TableCell>
{/*
<TableCell><Typography fontWeight={500}>{line.durationInMinutes} </Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.prepTimeInMinutes} </Typography></TableCell>
<TableCell><Typography fontWeight={500}>{line.postProdTimeInMinutes} </Typography></TableCell>
*/}
<TableCell>
<Typography fontWeight={500}>
{line.startTime && line.durationInMinutes
? dayjs(line.startTime)
.add(line.durationInMinutes, 'minute')
.format('MM-DD HH:mm')
: '-'}
</Typography>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="body2" >
{t("Processing Time")}: {line.durationInMinutes}{t("mins")}
@@ -662,7 +534,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
{t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")}
</Typography>
<Typography variant="body2" >

{t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")}
</Typography>
</Box>
@@ -689,53 +560,92 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<Chip label={t("Pending")} color="default" size="small" />
) : isPaused ? (
<Chip label={t("Paused")} color="warning" size="small" />
) : isPass ? (
<Chip label={t("Pass")} color="success" size="small" />
) : (
<Chip label={t("Unknown")} color="error" size="small" />
)}
)
}
</TableCell>
{!fromJosave&&(
<TableCell align="center">
{statusLower === 'pending' ? (
<Button
variant="contained"
size="small"
startIcon={<PlayArrowIcon />}
onClick={() => handleStartLineWithScan(line.id)}
>
{t("Start")}
</Button>
) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? (
<Button
variant="contained"
size="small"
startIcon={<CheckCircleIcon />}
onClick={async () => {
setSelectedLineId(line.id);
setIsExecutingLine(true);
await fetchProcessDetail();
}}
>
{t("View")}
</Button>
) : (
<Button
variant="outlined"
size="small"
onClick={async() => {
setSelectedLineId(line.id);
setIsExecutingLine(true);
await fetchProcessDetail();
}}
>
{t("View")}
</Button>
)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
<TableCell align="center">
<Stack direction="row" spacing={1} justifyContent="center">
{statusLower === 'pending' ? (
<>
<Button
variant="contained"
size="small"
startIcon={<PlayArrowIcon />}
onClick={() => handleStartLineWithScan(line.id)}
>
{t("Start")}
</Button>
<Button
variant="outlined"
size="small"
color="success"
onClick={() => handlePassLine(line.id)}
disabled={isPassDisabled}
>
{t("Pass")}
</Button>
</>
) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? (
<>
<Button
variant="contained"
size="small"
startIcon={<CheckCircleIcon />}
onClick={async () => {
setSelectedLineId(line.id);
setShowOutputPage(false);
setIsExecutingLine(true);
await fetchProcessDetail();
}}
>
{t("View")}
</Button>
<Button
variant="outlined"
size="small"
color="success"
onClick={() => handlePassLine(line.id)}
disabled={isPassDisabled}
>
{t("Pass")}
</Button>
</>
) : (
<>
<Button
variant="outlined"
size="small"
onClick={async() => {
setSelectedLineId(line.id);
setIsExecutingLine(true);
await fetchProcessDetail();
}}
>
{t("View")}
</Button>
<Button
variant="outlined"
size="small"
color="success"
onClick={() => handlePassLine(line.id)}
disabled={isPassDisabled}
>
{t("Pass")}
</Button>
</>
)}
</Stack>
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
) : (
@@ -743,13 +653,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<ProductionProcessStepExecution
lineId={selectedLineId}
onBack={handleBackFromStep}
//onClose={() => {
// setIsExecutingLine(false)
// setSelectedLineId(null)
//}}
//onOutputSubmitted={async () => {
// await fetchProcessDetail()
//}}
processData={processData} // ✅ 添加
allLines={lines} // ✅ 添加
jobOrderId={jobOrderId} // ✅ 添加
/>
)}
</Paper>
@@ -778,7 +684,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
<Box>
<Typography variant="body2" color="text.secondary">
{/* ✅ Show both options */}
{scannedEquipmentCode
? `${t("Equipment Code")}: ${scannedEquipmentCode}`
: t("Please scan equipment code")


+ 82
- 1
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx Vedi File

@@ -23,7 +23,7 @@ import {
} from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from "react-i18next";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority} from "@/app/api/jo/actions";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart} from "@/app/api/jo/actions";
import ProductionProcessDetail from "./ProductionProcessDetail";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
@@ -38,6 +38,9 @@ import { releaseJo, startJo } from "@/app/api/jo/actions";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
import ProcessSummaryHeader from "./ProcessSummaryHeader";
import EditIcon from "@mui/icons-material/Edit";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { dayjsToDateString } from "@/app/utils/formatUtil";

interface JobOrderLine {
id: number;
@@ -74,6 +77,9 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp
const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
const [operationPriority, setOperationPriority] = useState<number>(50);
const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false);
const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false);
const [planStartDate, setPlanStartDate] = useState<dayjs.Dayjs | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
@@ -126,6 +132,38 @@ const getStockAvailable = (line: JobOrderLine) => {
}
return line.stockQty || 0;
};
const handleOpenPlanStartDialog = useCallback(() => {
// 将 processData.date 转换为 dayjs 对象
if (processData?.date) {
// processData.date 可能是字符串或 Date 对象
setPlanStartDate(dayjs(processData.date));
} else {
setPlanStartDate(dayjs());
}
setOpenPlanStartDialog(true);
}, [processData?.date]);

const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
setOpenPlanStartDialog(false);
setPlanStartDate(null);
}, []);

const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
const response = await updateJoPlanStart({ id: jobOrderId, planStart });
if (response) {
await fetchData();
}
}, [fetchData]);

const handleConfirmPlanStart = useCallback(async () => {
if (!jobOrderId || !planStartDate) return;
// 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss)
const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`;
await handleUpdatePlanStart(jobOrderId, dateString);
setOpenPlanStartDialog(false);
setPlanStartDate(null);
}, [jobOrderId, planStartDate, handleUpdatePlanStart]);
const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
const response = await updateProductProcessPriority(productProcessId, productionPriority)
if (response) {
@@ -273,6 +311,15 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
label={t("Target Production Date")}
fullWidth
disabled={true}
InputProps={{
endAdornment: (processData?.jobOrderStatus === "planning" ? (
<InputAdornment position="end">
<IconButton size="small" onClick={handleOpenPlanStartDialog}>
<EditIcon fontSize="small" />
</IconButton>
</InputAdornment>
) : null),
}}
/>
</Grid>
<Grid item xs={6}>
@@ -600,6 +647,40 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
<Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
</DialogActions>
</Dialog>
<Dialog
open={openPlanStartDialog}
onClose={handleClosePlanStartDialog}
fullWidth
maxWidth="xs"
>
<DialogTitle>{t("Update Target Production Date")}</DialogTitle>
<DialogContent>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={t("Target Production Date")}
value={planStartDate}
onChange={(newValue) => setPlanStartDate(newValue)}
slotProps={{
textField: {
fullWidth: true,
margin: "dense",
autoFocus: true,
}
}}
/>
</LocalizationProvider>
</DialogContent>
<DialogActions>
<Button onClick={handleClosePlanStartDialog}>{t("Cancel")}</Button>
<Button
variant="contained"
onClick={handleConfirmPlanStart}
disabled={!planStartDate}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>


</Box>


+ 23
- 2
src/components/ProductionProcess/ProductionProcessList.tsx Vedi File

@@ -212,9 +212,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
<Card
sx={{
minHeight: 160,
maxHeight: 240,
maxHeight: 300,
display: "flex",
flexDirection: "column",
border: "1px solid",
borderColor: "success.main",
}}
>
<CardContent
@@ -238,11 +240,17 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
{t("Item Name")}: {process.itemCode} {process.itemName}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Required Qty")}: {process.requiredQty} {process.uom}
{t("Production Priority")}: {process.productionPriority}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Required Qty")}: {process.requiredQty} ({process.uom})
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Assume Time Need")}: {process.timeNeedToComplete} {t("minutes")}
</Typography>
{statusLower !== "pending" && linesWithStatus.length > 0 && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600}>
@@ -261,6 +269,19 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
)}
</Box>
)}
{statusLower == "pending" && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600} color= "white">
{t("t")}
</Typography>
<Box sx={{ mt: 1 }}>
<Typography variant="caption" color="text.secondary" display="block">
{""}
</Typography>
</Box>
</Box>
)}
</CardContent>

<CardActions sx={{ pt: 0.5 }}>


+ 179
- 118
src/components/ProductionProcess/ProductionProcessStepExecution.tsx Vedi File

@@ -27,24 +27,42 @@ import StopIcon from "@mui/icons-material/Stop";
import PauseIcon from "@mui/icons-material/Pause";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useTranslation } from "react-i18next";
import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest,saveProductProcessResumeTime,saveProductProcessIssueTime} from "@/app/api/jo/actions";
import {
JobOrderProcessLineDetailResponse,
updateProductProcessLineQty,
updateProductProcessLineQrscan,
fetchProductProcessLineDetail,
UpdateProductProcessLineQtyRequest,
saveProductProcessResumeTime,
saveProductProcessIssueTime,
ProductProcessWithLinesResponse, // ✅ 添加
ProductProcessLineResponse, // ✅ 添加
} from "@/app/api/jo/actions";
import { Operator, Machine } from "@/app/api/jo";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState, useMemo } from "react"; // ✅ 添加 useMemo
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入
import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入
import dayjs from "dayjs";
interface ProductionProcessStepExecutionProps {
lineId: number | null
onBack: () => void
//onClose: () => void
// onOutputSubmitted: () => Promise<void>
lineId: number | null;
onBack: () => void;
processData?: ProductProcessWithLinesResponse | null; // ✅ 添加
allLines?: ProductProcessLineResponse[]; // ✅ 添加
jobOrderId?: number; // ✅ 添加
}

const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({
lineId,
onBack,
processData, // ✅ 添加
allLines, // ✅ 添加
jobOrderId, // ✅ 添加
}) => {
const { t } = useTranslation( ["common","jo"]);
const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null);
const isCompleted = lineDetail?.status === "Completed";
const isCompleted = lineDetail?.status === "Completed" || lineDetail?.status === "Pass";
const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
byproductName: string;
byproductQty: number;
@@ -82,11 +100,34 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null);
const[isOpenReasonModel, setIsOpenReasonModel] = useState(false);
const [pauseReason, setPauseReason] = useState("");
// 检查是否两个都已扫描
//const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId;

// ✅ 添加:判断是否显示 Bag 表单的条件
const shouldShowBagForm = useMemo(() => {
if (!processData || !allLines || !lineDetail) return false;
// 检查 BOM description 是否为 "FG"
const bomDescription = processData.bomDescription;
if (bomDescription !== "FG") return false;
// 检查是否是最后一个 process line(按 seqNo 排序)
const sortedLines = [...allLines].sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0));
const maxSeqNo = sortedLines[sortedLines.length - 1]?.seqNo;
const isLastLine = lineDetail.seqNo === maxSeqNo;
return isLastLine;
}, [processData, allLines, lineDetail]);

// ✅ 添加:刷新 line detail 的函数
const handleRefreshLineDetail = useCallback(async () => {
if (lineId) {
try {
const detail = await fetchProductProcessLineDetail(lineId);
setLineDetail(detail as any);
} catch (error) {
console.error("Failed to refresh line detail", error);
}
}
}, [lineId]);

useEffect(() => {
if (!lineId) {
@@ -108,8 +149,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
setOutputData(prev => ({
...prev,
productProcessLineId: detail.id,
outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
outputFromProcessQty: (detail as any).outputFromProcessQty || 0,
outputFromProcessUom: (detail as any).outputFromProcessUom || "",
defectQty: detail.defectQty || 0,
defectUom: detail.defectUom || "",
scrapQty: detail.scrapQty || 0,
@@ -124,16 +165,16 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
setLineDetail(null);
});
}, [lineId]);

useEffect(() => {
// Don't show time remaining if completed
if (lineDetail?.status === "Completed") {
if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") {
console.log("Line is completed");
setRemainingTime(null);
setIsOverTime(false);
return;
}

// ✅ 问题1:添加详细的调试打印
console.log("🔍 Time Remaining Debug:", {
lineId: lineDetail?.id,
equipmentId: lineDetail?.equipmentId,
@@ -159,11 +200,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
return;
}

// Handle startTime format - it can be string or number array
let start: Date;
if (Array.isArray(lineDetail.startTime)) {
console.log("Line start time is an array:", lineDetail.startTime);
// If it's an array like [2025, 12, 15, 10, 30, 0], convert to Date
const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
start = new Date(year, month - 1, day, hour, minute, second);
} else {
@@ -171,7 +210,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
console.log("Line start time is a string:", lineDetail.startTime);
}

// Check if date is valid
if (isNaN(start.getTime())) {
console.error("Invalid startTime:", lineDetail.startTime);
setRemainingTime(null);
@@ -181,10 +219,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro

const durationMs = lineDetail.durationInMinutes * 60_000;
// Check if line is paused
const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused";
// ✅ 问题2:修复 stopTime 类型处理,像 startTime 一样处理
const parseStopTime = (stopTime: string | number[] | undefined): Date | null => {
if (!stopTime) return null;
@@ -198,20 +234,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const update = () => {
if (isPaused) {
// If paused, freeze the time at the last calculated value
// If we don't have a frozen value yet, calculate it based on stopTime
if (!frozenRemainingTime) {
// ✅ 修复问题2:正确处理 stopTime 的类型(string | number[])
const pauseTime = lineDetail.stopTime
? parseStopTime(lineDetail.stopTime)
: null;
// 如果没有 stopTime,使用当前时间(首次暂停时)
const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime())
? pauseTime
: new Date();
// ✅ 计算总暂停时间(所有已恢复的暂停记录)
const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
console.log("⏸️ Paused - calculating frozen time:", {
@@ -221,7 +252,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
totalPausedTimeMs: totalPausedTimeMs,
});
// ✅ 实际工作时间 = 暂停时间 - 开始时间 - 已恢复的暂停时间
const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs;
const remaining = durationMs - elapsed;
@@ -244,25 +274,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
console.log("⏸️ Frozen time:", frozenValue);
}
} else {
// ✅ 关键修复:暂停时始终使用冻结的值,不重新计算
setRemainingTime(frozenRemainingTime);
console.log("⏸️ Using frozen time:", frozenRemainingTime);
}
return;
}

// If resumed or in progress, clear frozen time and continue counting
if (frozenRemainingTime && !isPaused) {
console.log("▶️ Resumed - clearing frozen time");
setFrozenRemainingTime(null);
setLastPauseTime(null);
}

// ✅ 关键修复:计算剩余时间时,需要减去所有已恢复的暂停时间
const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0;
const now = new Date();
// ✅ 实际工作时间 = 当前时间 - 开始时间 - 所有已恢复的暂停时间
const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs;
const remaining = durationMs - elapsed;

@@ -276,7 +302,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
});

if (remaining <= 0) {
// Over time - show negative time in red
const overTime = Math.abs(remaining);
const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0");
const seconds = Math.floor((overTime % 60000) / 1000).toString().padStart(2, "0");
@@ -292,31 +317,25 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro

update();
// Only set interval if not paused
if (!isPaused) {
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}
}, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]);

// Reset frozen time when status changes from paused to in progress
useEffect(() => {
const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused";
const isNowInProgress = lineDetail?.status === "InProgress";
if (wasPaused && isNowInProgress && frozenRemainingTime) {
// When resuming, we need to account for the pause duration
// For now, we'll continue from the frozen time
// In a more accurate implementation, you'd fetch the issue details to get exact pause duration
setFrozenRemainingTime(null);
}
}, [lineDetail?.status, lineDetail?.productProcessIssueStatus]);

const handleSubmitOutput = async () => {
if (!lineDetail?.id) return;


try {
// 直接使用 actions.ts 中定义的函数
await updateProductProcessLineQty({
productProcessLineId: lineDetail?.id || 0 as number,
byproductName: outputData.byproductName,
@@ -324,7 +343,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
byproductUom: outputData.byproductUom,
outputFromProcessQty: outputData.outputFromProcessQty,
outputFromProcessUom: outputData.outputFromProcessUom,
// outputFromProcessUom: outputData.outputFromProcessUom,
defectQty: outputData.defectQty,
defectUom: outputData.defectUom,
defect2Qty: outputData.defect2Qty,
@@ -350,12 +368,11 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
productProcessIssueStatus: detail.productProcessIssueStatus
});
setLineDetail(detail as any);
// 初始化 outputData 从 lineDetail
setOutputData(prev => ({
...prev,
productProcessLineId: detail.id,
outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言
outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言
outputFromProcessQty: (detail as any).outputFromProcessQty || 0,
outputFromProcessUom: (detail as any).outputFromProcessUom || "",
defectQty: detail.defectQty || 0,
defectUom: detail.defectUom || "",
defectDescription: detail.defectDescription || "",
@@ -382,7 +399,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
}
};
// 处理 QR 码扫描效果
useEffect(() => {
if (isManualScanning && qrValues.length > 0 && lineDetail?.id) {
const latestQr = qrValues[qrValues.length - 1];
@@ -392,20 +408,88 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
}
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
//processQrCode(latestQr);
}
}, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]);

// 开始扫描

const lineAssumeEndTime = useMemo(() => {
if (!lineDetail?.startTime || !lineDetail?.durationInMinutes) return null;
// 解析 startTime(可能是数组或字符串)
let start: dayjs.Dayjs;
if (Array.isArray(lineDetail.startTime)) {
const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
start = dayjs(new Date(year, month - 1, day, hour, minute, second));
} else if (typeof lineDetail.startTime === 'string') {
// 检查是否是 "MM-DD HH:mm" 格式
const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/;
const match = lineDetail.startTime.match(mmddHhmmPattern);
if (match) {
const month = parseInt(match[1], 10);
const day = parseInt(match[2], 10);
const hour = parseInt(match[3], 10);
const minute = parseInt(match[4], 10);
// 使用当前年份,但如果跨年(startTime 是年末,当前是年初),使用上一年
const now = dayjs();
let year = now.year();
if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) {
year = now.year() - 1;
}
start = dayjs(new Date(year, month - 1, day, hour, minute, 0));
} else {
start = dayjs(lineDetail.startTime);
}
} else {
start = dayjs(lineDetail.startTime as any);
}
if (!start.isValid()) return null;
return start.add(lineDetail.durationInMinutes, 'minute');
}, [lineDetail?.startTime, lineDetail?.durationInMinutes]);
const lineStartTime = useMemo(() => {
if (!lineDetail?.startTime) return null;
let start: dayjs.Dayjs;
if (Array.isArray(lineDetail.startTime)) {
const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime;
start = dayjs(new Date(year, month - 1, day, hour, minute, second));
} else if (typeof lineDetail.startTime === 'string') {
const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/;
const match = lineDetail.startTime.match(mmddHhmmPattern);
if (match) {
const month = parseInt(match[1], 10);
const day = parseInt(match[2], 10);
const hour = parseInt(match[3], 10);
const minute = parseInt(match[4], 10);
const now = dayjs();
let year = now.year();
if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) {
year = now.year() - 1;
}
start = dayjs(new Date(year, month - 1, day, hour, minute, 0));
} else {
start = dayjs(lineDetail.startTime);
}
} else {
start = dayjs(lineDetail.startTime as any);
}
return start.isValid() ? start : null;
}, [lineDetail?.startTime]);
const handleOpenReasonModel = () => {
setIsOpenReasonModel(true);
setPauseReason(""); // 重置原因
setPauseReason("");
};
const handleCloseReasonModel = () => {
setIsOpenReasonModel(false);
setPauseReason(""); // 清空原因
setPauseReason("");
};
const handleSaveReason = async () => {
if (!pauseReason.trim()) {
@@ -421,7 +505,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
});
setIsOpenReasonModel(false);
setPauseReason("");
// 刷新 line detail
fetchProductProcessLineDetail(lineDetail.id)
.then((detail) => {
setLineDetail(detail as any);
@@ -435,7 +518,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
}
};

// ✅ Add this new handler for resume
const handleResume = async () => {
if (!lineDetail?.productProcessIssueId) {
console.error("No productProcessIssueId found");
@@ -446,13 +528,11 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
await saveProductProcessResumeTime(lineDetail.productProcessIssueId);
console.log("✅ Resume API called successfully");
// ✅ Refresh line detail after resume
if (lineDetail?.id) {
fetchProductProcessLineDetail(lineDetail.id)
.then((detail) => {
console.log("✅ Line detail refreshed after resume:", detail);
setLineDetail(detail as any);
// Clear frozen time when resuming
setFrozenRemainingTime(null);
setLastPauseTime(null);
})
@@ -465,6 +545,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
alert(t("Failed to resume. Please try again."));
}
};

return (
<Box>
<Box sx={{ mb: 2 }}>
@@ -472,18 +553,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
{t("Back to List")}
</Button>
</Box>
{/* 如果已完成,显示合并的视图 */}
{processData && (
<OverallTimeRemainingCard processData={processData} />
)}
{isCompleted ? (
<Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}>
<CardContent>
{lineDetail?.status === "Pass" ? (
<Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
{t("Passed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
</Typography>
) : (
<Typography variant="h5" color="success.main" gutterBottom fontWeight="bold">
{t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
</Typography>
{/*<Divider sx={{ my: 2 }} />*/}
{/* 步骤信息部分 */}
{t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo})
</Typography>
)}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
{t("Step Information")}
</Typography>
@@ -510,9 +594,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</Grid>
</Grid>

{/*<Divider sx={{ my: 2 }} />*/}

{/* 产出数据部分 */}
<Typography variant="h6" gutterBottom sx={{ mt: 2 }}>
{t("Production Output Data")}
</Typography>
@@ -526,7 +607,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableRow>
</TableHead>
<TableBody>
{/* Output from Process */}
<TableRow>
<TableCell>
<Typography fontWeight={500}>{t("Output from Process")}</Typography>
@@ -539,27 +619,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableCell>
</TableRow>
{/* By-product */}
{/*
<TableRow>
<TableCell>
<Typography fontWeight={500}>{t("By-product")}</Typography>
{lineDetail.byproductName && (
<Typography variant="caption" color="text.secondary">
({lineDetail.byproductName})
</Typography>
)}
</TableCell>
<TableCell>
<Typography>{lineDetail.byproductQty}</Typography>
</TableCell>
<TableCell>
<Typography>{lineDetail.byproductUom || "-"}</Typography>
</TableCell>
</TableRow>
*/}
{/* Defect */}

<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
@@ -573,7 +632,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TableCell>
<Typography>{lineDetail.defectDescription || "-"}</Typography>
</TableCell>
</TableRow>
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
@@ -588,7 +646,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TableCell>
<Typography>{lineDetail.defectDescription3 || "-"}</Typography>
</TableCell>
</TableRow>
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
@@ -603,9 +660,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TableCell>
<Typography>{lineDetail.defectDescription2 || "-"}</Typography>
</TableCell>
</TableRow>
{/* Scrap */}
<TableRow sx={{ bgcolor: 'error.50' }}>
<TableCell>
<Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
@@ -623,8 +678,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</Card>
) : (
<>
{/* 如果未完成,显示原来的两个部分 */}
{/* 当前步骤信息 */}
{!showOutputTable && (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} >
@@ -654,6 +707,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
>
{isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime}
</Typography>
{/* ✅ 添加:Process Start Time 和 Assume End Time */}
{/* ✅ 添加:Process Start Time 和 Assume End Time */}
{processData?.startTime && (
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
<strong>{t("Process Start Time")}:</strong> {dayjs(processData.startTime).format("MM-DD")} {dayjs(processData.startTime).format("HH:mm")}
</Typography>
</Box>
)}
{lineStartTime && (
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
<strong>{t("Step Start Time")}:</strong> {lineStartTime.format("MM-DD")} {lineStartTime.format("HH:mm")}
</Typography>
{lineAssumeEndTime && (
<Typography variant="body2" color="text.secondary">
<strong>{t("Assume End Time")}:</strong> {lineAssumeEndTime.format("MM-DD")} {lineAssumeEndTime.format("HH:mm")}
</Typography>
)}
</Box>
)}
{lineDetail?.status === "Paused" && (
<Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}>
{t("Timer Paused")}
@@ -662,17 +736,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</Box>
)}
<Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}>
{/*
<Button
variant="contained"
color="error"
startIcon={<StopIcon />}
onClick={() => saveProductProcessIssueTime(lineDetail?.id || 0 as number)}
>
{t("Stop")}
</Button>
*/
}
{ lineDetail?.status === 'InProgress'? (
<Button
variant="contained"
@@ -687,7 +750,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained"
color="success"
startIcon={<PlayArrowIcon />}
onClick={handleResume} // ✅ Change from inline call to handler
onClick={handleResume}
>
{t("Continue")}
</Button>
@@ -710,8 +773,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
{/* ========== 产出输入表单 ========== */}
{showOutputTable && (
<Box>
<Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
<Table size="small">
<TableHead>
@@ -723,7 +784,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableRow>
</TableHead>
<TableBody>
{/* start line output */}
<TableRow>
<TableCell>
<Typography fontWeight={500}>{t("Output from Process")}</Typography>
@@ -756,8 +816,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableCell>
</TableRow>

{/* defect 1 */}
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(1)")}</Typography>
@@ -789,7 +847,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TextField
fullWidth
size="small"
//value={outputData.defectUom}
onChange={(e) => setOutputData({
...outputData,
defectDescription: e.target.value
@@ -797,7 +854,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
/>
</TableCell>
</TableRow>
{/* defect 2 */}
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography>
@@ -829,7 +885,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TextField
fullWidth
size="small"
//value={outputData.defectUom}
onChange={(e) => setOutputData({
...outputData,
defectDescription2: e.target.value
@@ -837,7 +892,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
/>
</TableCell>
</TableRow>
{/* defect 3 */}
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography>
@@ -869,7 +923,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
<TextField
fullWidth
size="small"
//value={outputData.defectUom}
onChange={(e) => setOutputData({
...outputData,
defectDescription3: e.target.value
@@ -877,7 +930,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
/>
</TableCell>
</TableRow>
{/* scrap */}
<TableRow sx={{ bgcolor: 'error.50' }}>
<TableCell>
<Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
@@ -909,7 +961,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</TableBody>
</Table>

{/* submit button */}
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
variant="outlined"
@@ -928,6 +979,17 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
</Paper>
</Box>
)}

{/* ========== Bag Consumption Form ========== */}
{((showOutputTable || isCompleted) && shouldShowBagForm && jobOrderId && lineId) && (
<BagConsumptionForm
jobOrderId={jobOrderId}
lineId={lineId}
bomDescription={processData?.bomDescription}
isLastLine={shouldShowBagForm}
onRefresh={handleRefreshLineDetail}
/>
)}
</>
)}
<Dialog
@@ -947,7 +1009,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
rows={4}
value={pauseReason}
onChange={(e) => setPauseReason(e.target.value)}
//required
/>
</DialogContent>
<DialogActions>


+ 24
- 18
src/components/Qc/QcComponent.tsx Vedi File

@@ -142,28 +142,25 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => {
if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") {
setError("acceptQty", { message: t("value must be a number") });
} else
if (!Number.isInteger(accQty)) {
setError("acceptQty", { message: t("value must be integer") });
}
if (accQty > itemDetail.acceptedQty) {
setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty}` });
} else
if (accQty < 1) {
if (accQty <= 0) {
setError("acceptQty", { message: t("minimal value is 1") });
} else
console.log("%c Validated accQty:", "color:yellow", accQty);
}

},[setError, qcDecision, accQty, itemDetail])
useEffect(() => { // W I P // -----
if (qcDecision == 1) {
if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty}`)) return;
if (validateFieldFail("acceptQty", accQty < 1, t("minimal value is 1"))) return;
if (validateFieldFail("acceptQty", isNaN(accQty), t("value must be a number"))) return;
}
useEffect(() => { // W I P // -----
if (qcDecision == 1) {
if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${
itemDetail.acceptedQty}`)) return;
if (validateFieldFail("acceptQty", accQty <= 0, t("minimal value is 1"))) return;
if (validateFieldFail("acceptQty", isNaN(accQty), t("value must be a number"))) return;
}

const qcResultItems = qcResult; //console.log("Validating:", qcResultItems);
@@ -586,17 +583,26 @@ useEffect(() => {
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target.value;

const numReg = /^[0-9]+$/
// 允许数字和小数点,但只允许一个小数点
const numReg = /^\d+(\.\d*)?$/
let r = '';
if (!numReg.test(input)) {
const result = input.replace(/\D/g, "");
r = (result === '' ? result : Number(result)).toString();
if (input === '' || input === '.') {
r = input;
} else if (!numReg.test(input)) {
// 移除非数字字符,但保留一个小数点
let result = input.replace(/[^0-9.]/g, '');
// 确保只有一个小数点
const parts = result.split('.');
if (parts.length > 2) {
result = parts[0] + '.' + parts.slice(1).join('');
}
r = result;
} else {
r = Number(input).toString()
r = input;
}
e.target.value = r;
}}
inputProps={{ min: 1, max:itemDetail.acceptedQty }}
inputProps={{ min: 0.01, max:itemDetail.acceptedQty, step: 0.01 }}
// onChange={(e) => {
// const inputValue = e.target.value;
// if (inputValue === '' || /^[0-9]*$/.test(inputValue)) {


Caricamento…
Annulla
Salva