| @@ -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), | |||||
| } | |||||
| ); | |||||
| }); | |||||
| @@ -16,6 +16,7 @@ export interface SaveJo { | |||||
| type: string; | type: string; | ||||
| //jobType?: string; | //jobType?: string; | ||||
| jobTypeId?: number; | jobTypeId?: number; | ||||
| productionPriority?: number; | |||||
| } | } | ||||
| export interface SaveJoResponse { | export interface SaveJoResponse { | ||||
| @@ -246,6 +247,7 @@ export interface ProductProcessWithLinesResponse { | |||||
| jobOrderId?: number; | jobOrderId?: number; | ||||
| jobOrderCode: string; | jobOrderCode: string; | ||||
| jobOrderStatus: string; | jobOrderStatus: string; | ||||
| bomDescription: string; | |||||
| jobType: string; | jobType: string; | ||||
| isDark: string; | isDark: string; | ||||
| isDense: number; | isDense: number; | ||||
| @@ -321,6 +323,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||||
| date: string; | date: string; | ||||
| matchStatus: string; | matchStatus: string; | ||||
| bomId?: number; | bomId?: number; | ||||
| productionPriority: number; | |||||
| assignedTo: number; | assignedTo: number; | ||||
| pickOrderId: number; | pickOrderId: number; | ||||
| pickOrderStatus: string; | pickOrderStatus: string; | ||||
| @@ -328,6 +331,7 @@ export interface AllJoborderProductProcessInfoResponse { | |||||
| itemName: string; | itemName: string; | ||||
| requiredQty: number; | requiredQty: number; | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| timeNeedToComplete: number; | |||||
| uom: string; | uom: string; | ||||
| stockInLineId: number; | stockInLineId: number; | ||||
| jobOrderCode: string; | jobOrderCode: string; | ||||
| @@ -578,6 +582,11 @@ export interface JobOrderListForPrintQrCodeResponse { | |||||
| stockOutLineStatus: string; | stockOutLineStatus: string; | ||||
| finihedTime: 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) => { | export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, | `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, | ||||
| @@ -1086,4 +1095,38 @@ export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Pr | |||||
| ); | ); | ||||
| return reportBlob; | 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" }, | |||||
| } | |||||
| ); | |||||
| }; | }; | ||||
| @@ -32,6 +32,7 @@ export interface JobOrder { | |||||
| jobTypeName: string; | jobTypeName: string; | ||||
| sufficientCount: number; | sufficientCount: number; | ||||
| insufficientCount: number; | insufficientCount: number; | ||||
| productionPriority: number; | |||||
| // TODO pack below into StockInLineInfo | // TODO pack below into StockInLineInfo | ||||
| stockInLineId?: number; | stockInLineId?: number; | ||||
| stockInLineStatus?: string; | stockInLineStatus?: string; | ||||
| @@ -8,7 +8,7 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi | |||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import { isFinite } from "lodash"; | 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 { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { msg } from "../Swal/CustomAlerts"; | import { msg } from "../Swal/CustomAlerts"; | ||||
| @@ -30,17 +30,52 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| onSearch, | onSearch, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| const [multiplier, setMultiplier] = useState<number>(1); | |||||
| const formProps = useForm<SaveJo>({ | const formProps = useForm<SaveJo>({ | ||||
| mode: "onChange", | mode: "onChange", | ||||
| defaultValues: { | |||||
| productionPriority: 50 | |||||
| } | |||||
| }); | }); | ||||
| const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps | const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps | ||||
| // 监听 bomId 变化 | // 监听 bomId 变化 | ||||
| const selectedBomId = watch("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(() => { | const onModalClose = useCallback(() => { | ||||
| reset() | reset() | ||||
| onClose() | onClose() | ||||
| setMultiplier(1); | |||||
| }, [reset, onClose]) | }, [reset, onClose]) | ||||
| const handleAutoCompleteChange = useCallback( | const handleAutoCompleteChange = useCallback( | ||||
| @@ -61,65 +96,7 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| [formProps] | [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 | // 当 BOM 改变时,自动选择匹配的 Job Type | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (!selectedBomId) { | if (!selectedBomId) { | ||||
| @@ -174,6 +151,10 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day')) | data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day')) | ||||
| } | } | ||||
| data.jobTypeId = Number(data.jobTypeId); | data.jobTypeId = Number(data.jobTypeId); | ||||
| // 如果 productionPriority 为空或无效,使用默认值 50 | |||||
| data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority) | |||||
| ? Number(data.productionPriority) | |||||
| : 50; | |||||
| const response = await manualCreateJo(data) | const response = await manualCreateJo(data) | ||||
| if (response) { | if (response) { | ||||
| onSearch(); | onSearch(); | ||||
| @@ -283,31 +264,73 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| render={({ field, fieldState: { error } }) => { | render={({ field, fieldState: { error } }) => { | ||||
| const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId")); | const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId")); | ||||
| const uom = selectedBom?.outputQtyUom || ""; | const uom = selectedBom?.outputQtyUom || ""; | ||||
| const outputQty = selectedBom?.outputQty ?? 0; | |||||
| const calculatedValue = multiplier * outputQty; | |||||
| return ( | 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> | ||||
| <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}> | <Grid item xs={12} sm={12} md={6}> | ||||
| <Controller | <Controller | ||||
| control={control} | control={control} | ||||
| @@ -1,5 +1,5 @@ | |||||
| "use client" | "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 React, { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Criterion } from "../SearchBox"; | import { Criterion } from "../SearchBox"; | ||||
| @@ -12,10 +12,11 @@ import { useRouter } from "next/navigation"; | |||||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { StockInLineInput } from "@/app/api/stockIn"; | import { StockInLineInput } from "@/app/api/stockIn"; | ||||
| import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; | 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 { BomCombo } from "@/app/api/bom"; | ||||
| import JoCreateFormModal from "./JoCreateFormModal"; | import JoCreateFormModal from "./JoCreateFormModal"; | ||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | |||||
| import QcStockInModal from "../Qc/QcStockInModal"; | import QcStockInModal from "../Qc/QcStockInModal"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| @@ -26,6 +27,10 @@ import { fetchInventories } from "@/app/api/inventory/actions"; | |||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | 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 { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest, | defaultInputs: SearchJoResultRequest, | ||||
| @@ -50,7 +55,14 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | ||||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | 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 fetchJoDetailClient = async (id: number): Promise<JobOrder> => { | ||||
| const response = await fetch(`/api/jo/detail?id=${id}`); | const response = await fetch(`/api/jo/detail?id=${id}`); | ||||
| if (!response.ok) { | if (!response.ok) { | ||||
| @@ -98,6 +110,32 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| fetchInventoryData(); | 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 getStockAvailable = (pickLine: JoDetailPickLine) => { | ||||
| const inventory = inventoryData.find(inventory => | const inventory = inventoryData.find(inventory => | ||||
| @@ -137,14 +175,71 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| options: jobTypes.map(jt => jt.name) | options: jobTypes.map(jt => jt.name) | ||||
| }, | }, | ||||
| ], [t, jobTypes]) | ], [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>[]>( | 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", | name: "code", | ||||
| label: t("Code"), | label: t("Code"), | ||||
| flex: 2 | flex: 2 | ||||
| }, | }, | ||||
| { | { | ||||
| name: "item", | name: "item", | ||||
| label: `${t("Item Name")}`, | 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) : '-' | 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, | name: "stockStatus" as keyof JobOrder, | ||||
| label: t("BOM Status"), | 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", | name: "jobTypeName", | ||||
| label: t("Job Type"), | 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 函数 | // 按照 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 逻辑 | // 按照 PoSearch 的模式:使用相同的 useEffect 逻辑 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| newPageFetch(pagingController, inputs); | newPageFetch(pagingController, inputs); | ||||
| @@ -352,6 +452,31 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| setPagingController(defaultPagingController); | setPagingController(defaultPagingController); | ||||
| }, [defaultInputs]) | }, [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(() => { | const onReset = useCallback(() => { | ||||
| setInputs(defaultInputs); | setInputs(defaultInputs); | ||||
| @@ -413,7 +538,66 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| inputDetail={modalInfo} | inputDetail={modalInfo} | ||||
| printerCombo={printerCombo} | 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; | export default JoSearch; | ||||
| @@ -203,32 +203,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); | fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); | ||||
| }, [fetchNewPageConsoPickOrder, 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 => { | const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { | ||||
| let isReleasable = true; | let isReleasable = true; | ||||
| @@ -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; | |||||
| @@ -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; | |||||
| @@ -2,21 +2,29 @@ import { Card, CardContent, Stack, Typography } from "@mui/material"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { ProductProcessWithLinesResponse } from "@/app/api/jo/actions"; | |||||
| interface Props { | 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 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 ( | return ( | ||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <CardContent> | ||||
| @@ -38,6 +46,33 @@ const ProcessSummaryHeader: React.FC<Props> = ({ processData }) => { | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | <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> | {t("Production Date")}: <strong style={{ color: "green" }}>{processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""}</strong> | ||||
| </Typography> | </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> | </Stack> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| @@ -39,8 +39,13 @@ import { | |||||
| JobOrderProcessLineDetailResponse, | JobOrderProcessLineDetailResponse, | ||||
| ProductProcessLineInfoResponse, | ProductProcessLineInfoResponse, | ||||
| startProductProcessLine, | startProductProcessLine, | ||||
| fetchProductProcessesByJobOrderId | |||||
| fetchProductProcessesByJobOrderId, | |||||
| ProductProcessWithLinesResponse, // ✅ 添加 | |||||
| ProductProcessLineResponse, | |||||
| passProductProcessLine, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { updateProductProcessLineStatus } from "@/app/api/jo/actions"; | |||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; | import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; | ||||
| import ProductionOutputFormPage from "./ProductionOutputFormPage"; | import ProductionOutputFormPage from "./ProductionOutputFormPage"; | ||||
| @@ -62,8 +67,8 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const [showOutputPage, setShowOutputPage] = useState(false); | 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); | const [loading, setLoading] = useState(false); | ||||
| // 选中的 line 和执行状态 | // 选中的 line 和执行状态 | ||||
| @@ -125,7 +130,7 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| setProcessData(currentProcess); | setProcessData(currentProcess); | ||||
| // 使用 productProcessLines 字段(API 返回的字段名) | // 使用 productProcessLines 字段(API 返回的字段名) | ||||
| const lines = (currentProcess as any).productProcessLines || []; | |||||
| const lines = currentProcess.productProcessLines || []; | |||||
| setLines(lines); | setLines(lines); | ||||
| console.log(" Process data loaded:", currentProcess); | console.log(" Process data loaded:", currentProcess); | ||||
| @@ -143,105 +148,27 @@ const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||||
| fetchProcessDetail(); | fetchProcessDetail(); | ||||
| }, [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 { | 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) => { | 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; | return; | ||||
| } | } | ||||
| @@ -334,7 +261,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| const effectiveEquipmentCode = | const effectiveEquipmentCode = | ||||
| scannedEquipmentCode ?? null; | scannedEquipmentCode ?? null; | ||||
| console.log("Submitting scan data with equipmentCode:", { | console.log("Submitting scan data with equipmentCode:", { | ||||
| productProcessLineId: lineId, | productProcessLineId: lineId, | ||||
| staffNo: scannedStaffNo, | staffNo: scannedStaffNo, | ||||
| @@ -538,53 +465,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| return ( | return ( | ||||
| <Box> | <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 ========== */} | {/* ========== 第二部分:Process Lines ========== */} | ||||
| <Paper sx={{ p: 3 }}> | <Paper sx={{ p: 3 }}> | ||||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | <Typography variant="h6" gutterBottom fontWeight="bold"> | ||||
| @@ -602,25 +482,12 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <TableCell>{t("Description")}</TableCell> | <TableCell>{t("Description")}</TableCell> | ||||
| <TableCell>{t("EquipmentType-EquipmentName-Code")}</TableCell> | <TableCell>{t("EquipmentType-EquipmentName-Code")}</TableCell> | ||||
| <TableCell>{t("Operator")}</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> | <TableCell> | ||||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | ||||
| <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | <Typography variant="subtitle2" sx={{ fontWeight: 600 }}> | ||||
| {t("Time Information(mins)")} | {t("Time Information(mins)")} | ||||
| </Typography> | </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> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="center">{t("Status")}</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 isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; | ||||
| const isPaused = statusLower === 'paused'; | const isPaused = statusLower === 'paused'; | ||||
| const isPending = statusLower === 'pending' || status === ''; | const isPending = statusLower === 'pending' || status === ''; | ||||
| const isPass = statusLower === 'pass'; | |||||
| const isPassDisabled = isCompleted || isPass; | |||||
| return ( | return ( | ||||
| <TableRow key={line.id}> | <TableRow key={line.id}> | ||||
| <TableCell>{line.seqNo}</TableCell> | <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.description || "-"}</Typography></TableCell> | ||||
| <TableCell><Typography fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography></TableCell> | <TableCell><Typography fontWeight={500}>{line.equipmentDetailCode||equipmentName}</Typography></TableCell> | ||||
| <TableCell><Typography fontWeight={500}>{line.operatorName}</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> | <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 }}> | <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | ||||
| <Typography variant="body2" > | <Typography variant="body2" > | ||||
| {t("Processing Time")}: {line.durationInMinutes}{t("mins")} | {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")} | {t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" > | <Typography variant="body2" > | ||||
| {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} | {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} | ||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| @@ -689,53 +560,92 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <Chip label={t("Pending")} color="default" size="small" /> | <Chip label={t("Pending")} color="default" size="small" /> | ||||
| ) : isPaused ? ( | ) : isPaused ? ( | ||||
| <Chip label={t("Paused")} color="warning" size="small" /> | <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" /> | <Chip label={t("Unknown")} color="error" size="small" /> | ||||
| )} | |||||
| ) | |||||
| } | |||||
| </TableCell> | </TableCell> | ||||
| {!fromJosave&&( | {!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> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| ) : ( | ) : ( | ||||
| @@ -743,13 +653,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <ProductionProcessStepExecution | <ProductionProcessStepExecution | ||||
| lineId={selectedLineId} | lineId={selectedLineId} | ||||
| onBack={handleBackFromStep} | onBack={handleBackFromStep} | ||||
| //onClose={() => { | |||||
| // setIsExecutingLine(false) | |||||
| // setSelectedLineId(null) | |||||
| //}} | |||||
| //onOutputSubmitted={async () => { | |||||
| // await fetchProcessDetail() | |||||
| //}} | |||||
| processData={processData} // ✅ 添加 | |||||
| allLines={lines} // ✅ 添加 | |||||
| jobOrderId={jobOrderId} // ✅ 添加 | |||||
| /> | /> | ||||
| )} | )} | ||||
| </Paper> | </Paper> | ||||
| @@ -778,7 +684,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| <Box> | <Box> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {/* ✅ Show both options */} | |||||
| {scannedEquipmentCode | {scannedEquipmentCode | ||||
| ? `${t("Equipment Code")}: ${scannedEquipmentCode}` | ? `${t("Equipment Code")}: ${scannedEquipmentCode}` | ||||
| : t("Please scan equipment code") | : t("Please scan equipment code") | ||||
| @@ -23,7 +23,7 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import { useTranslation } from "react-i18next"; | 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 ProductionProcessDetail from "./ProductionProcessDetail"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; | 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 JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | ||||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | import ProcessSummaryHeader from "./ProcessSummaryHeader"; | ||||
| import EditIcon from "@mui/icons-material/Edit"; | 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 { | interface JobOrderLine { | ||||
| id: number; | id: number; | ||||
| @@ -74,6 +77,9 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | ||||
| const [operationPriority, setOperationPriority] = useState<number>(50); | const [operationPriority, setOperationPriority] = useState<number>(50); | ||||
| const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | ||||
| const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); | |||||
| const [planStartDate, setPlanStartDate] = useState<dayjs.Dayjs | null>(null); | |||||
| const fetchData = useCallback(async () => { | const fetchData = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| @@ -126,6 +132,38 @@ const getStockAvailable = (line: JobOrderLine) => { | |||||
| } | } | ||||
| return line.stockQty || 0; | 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 handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { | ||||
| const response = await updateProductProcessPriority(productProcessId, productionPriority) | const response = await updateProductProcessPriority(productProcessId, productionPriority) | ||||
| if (response) { | if (response) { | ||||
| @@ -273,6 +311,15 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| label={t("Target Production Date")} | label={t("Target Production Date")} | ||||
| fullWidth | fullWidth | ||||
| disabled={true} | disabled={true} | ||||
| InputProps={{ | |||||
| endAdornment: (processData?.jobOrderStatus === "planning" ? ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton size="small" onClick={handleOpenPlanStartDialog}> | |||||
| <EditIcon fontSize="small" /> | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ) : null), | |||||
| }} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| @@ -600,6 +647,40 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button> | <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </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> | </Box> | ||||
| @@ -212,9 +212,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| <Card | <Card | ||||
| sx={{ | sx={{ | ||||
| minHeight: 160, | minHeight: 160, | ||||
| maxHeight: 240, | |||||
| maxHeight: 300, | |||||
| display: "flex", | display: "flex", | ||||
| flexDirection: "column", | flexDirection: "column", | ||||
| border: "1px solid", | |||||
| borderColor: "success.main", | |||||
| }} | }} | ||||
| > | > | ||||
| <CardContent | <CardContent | ||||
| @@ -238,11 +240,17 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| {t("Item Name")}: {process.itemCode} {process.itemName} | {t("Item Name")}: {process.itemCode} {process.itemName} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | <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> | ||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} | {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("Assume Time Need")}: {process.timeNeedToComplete} {t("minutes")} | |||||
| </Typography> | |||||
| {statusLower !== "pending" && linesWithStatus.length > 0 && ( | {statusLower !== "pending" && linesWithStatus.length > 0 && ( | ||||
| <Box sx={{ mt: 1 }}> | <Box sx={{ mt: 1 }}> | ||||
| <Typography variant="body2" fontWeight={600}> | <Typography variant="body2" fontWeight={600}> | ||||
| @@ -261,6 +269,19 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| )} | )} | ||||
| </Box> | </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> | </CardContent> | ||||
| <CardActions sx={{ pt: 0.5 }}> | <CardActions sx={{ pt: 0.5 }}> | ||||
| @@ -27,24 +27,42 @@ import StopIcon from "@mui/icons-material/Stop"; | |||||
| import PauseIcon from "@mui/icons-material/Pause"; | import PauseIcon from "@mui/icons-material/Pause"; | ||||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | ||||
| import { useTranslation } from "react-i18next"; | 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 { 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 { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 | |||||
| import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 | |||||
| import dayjs from "dayjs"; | |||||
| interface ProductionProcessStepExecutionProps { | 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> = ({ | const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionProps> = ({ | ||||
| lineId, | lineId, | ||||
| onBack, | onBack, | ||||
| processData, // ✅ 添加 | |||||
| allLines, // ✅ 添加 | |||||
| jobOrderId, // ✅ 添加 | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation( ["common","jo"]); | const { t } = useTranslation( ["common","jo"]); | ||||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | 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 & { | const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | ||||
| byproductName: string; | byproductName: string; | ||||
| byproductQty: number; | byproductQty: number; | ||||
| @@ -82,11 +100,34 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null); | const [lastPauseTime, setLastPauseTime] = useState<Date | null>(null); | ||||
| const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | ||||
| const [pauseReason, setPauseReason] = useState(""); | 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(() => { | useEffect(() => { | ||||
| if (!lineId) { | if (!lineId) { | ||||
| @@ -108,8 +149,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| setOutputData(prev => ({ | setOutputData(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | 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, | defectQty: detail.defectQty || 0, | ||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| scrapQty: detail.scrapQty || 0, | scrapQty: detail.scrapQty || 0, | ||||
| @@ -124,16 +165,16 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| setLineDetail(null); | setLineDetail(null); | ||||
| }); | }); | ||||
| }, [lineId]); | }, [lineId]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| // Don't show time remaining if completed | // Don't show time remaining if completed | ||||
| if (lineDetail?.status === "Completed") { | |||||
| if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") { | |||||
| console.log("Line is completed"); | console.log("Line is completed"); | ||||
| setRemainingTime(null); | setRemainingTime(null); | ||||
| setIsOverTime(false); | setIsOverTime(false); | ||||
| return; | return; | ||||
| } | } | ||||
| // ✅ 问题1:添加详细的调试打印 | |||||
| console.log("🔍 Time Remaining Debug:", { | console.log("🔍 Time Remaining Debug:", { | ||||
| lineId: lineDetail?.id, | lineId: lineDetail?.id, | ||||
| equipmentId: lineDetail?.equipmentId, | equipmentId: lineDetail?.equipmentId, | ||||
| @@ -159,11 +200,9 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| return; | return; | ||||
| } | } | ||||
| // Handle startTime format - it can be string or number array | |||||
| let start: Date; | let start: Date; | ||||
| if (Array.isArray(lineDetail.startTime)) { | if (Array.isArray(lineDetail.startTime)) { | ||||
| console.log("Line start time is an array:", 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; | const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; | ||||
| start = new Date(year, month - 1, day, hour, minute, second); | start = new Date(year, month - 1, day, hour, minute, second); | ||||
| } else { | } else { | ||||
| @@ -171,7 +210,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| console.log("Line start time is a string:", lineDetail.startTime); | console.log("Line start time is a string:", lineDetail.startTime); | ||||
| } | } | ||||
| // Check if date is valid | |||||
| if (isNaN(start.getTime())) { | if (isNaN(start.getTime())) { | ||||
| console.error("Invalid startTime:", lineDetail.startTime); | console.error("Invalid startTime:", lineDetail.startTime); | ||||
| setRemainingTime(null); | setRemainingTime(null); | ||||
| @@ -181,10 +219,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const durationMs = lineDetail.durationInMinutes * 60_000; | const durationMs = lineDetail.durationInMinutes * 60_000; | ||||
| // Check if line is paused | |||||
| const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused"; | const isPaused = lineDetail.status === "Paused" || lineDetail.productProcessIssueStatus === "Paused"; | ||||
| // ✅ 问题2:修复 stopTime 类型处理,像 startTime 一样处理 | |||||
| const parseStopTime = (stopTime: string | number[] | undefined): Date | null => { | const parseStopTime = (stopTime: string | number[] | undefined): Date | null => { | ||||
| if (!stopTime) return null; | if (!stopTime) return null; | ||||
| @@ -198,20 +234,15 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const update = () => { | const update = () => { | ||||
| if (isPaused) { | 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) { | if (!frozenRemainingTime) { | ||||
| // ✅ 修复问题2:正确处理 stopTime 的类型(string | number[]) | |||||
| const pauseTime = lineDetail.stopTime | const pauseTime = lineDetail.stopTime | ||||
| ? parseStopTime(lineDetail.stopTime) | ? parseStopTime(lineDetail.stopTime) | ||||
| : null; | : null; | ||||
| // 如果没有 stopTime,使用当前时间(首次暂停时) | |||||
| const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime()) | const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime()) | ||||
| ? pauseTime | ? pauseTime | ||||
| : new Date(); | : new Date(); | ||||
| // ✅ 计算总暂停时间(所有已恢复的暂停记录) | |||||
| const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | ||||
| console.log("⏸️ Paused - calculating frozen time:", { | console.log("⏸️ Paused - calculating frozen time:", { | ||||
| @@ -221,7 +252,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| totalPausedTimeMs: totalPausedTimeMs, | totalPausedTimeMs: totalPausedTimeMs, | ||||
| }); | }); | ||||
| // ✅ 实际工作时间 = 暂停时间 - 开始时间 - 已恢复的暂停时间 | |||||
| const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs; | const elapsed = pauseTimeToUse.getTime() - start.getTime() - totalPausedTimeMs; | ||||
| const remaining = durationMs - elapsed; | const remaining = durationMs - elapsed; | ||||
| @@ -244,25 +274,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| console.log("⏸️ Frozen time:", frozenValue); | console.log("⏸️ Frozen time:", frozenValue); | ||||
| } | } | ||||
| } else { | } else { | ||||
| // ✅ 关键修复:暂停时始终使用冻结的值,不重新计算 | |||||
| setRemainingTime(frozenRemainingTime); | setRemainingTime(frozenRemainingTime); | ||||
| console.log("⏸️ Using frozen time:", frozenRemainingTime); | console.log("⏸️ Using frozen time:", frozenRemainingTime); | ||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| // If resumed or in progress, clear frozen time and continue counting | |||||
| if (frozenRemainingTime && !isPaused) { | if (frozenRemainingTime && !isPaused) { | ||||
| console.log("▶️ Resumed - clearing frozen time"); | console.log("▶️ Resumed - clearing frozen time"); | ||||
| setFrozenRemainingTime(null); | setFrozenRemainingTime(null); | ||||
| setLastPauseTime(null); | setLastPauseTime(null); | ||||
| } | } | ||||
| // ✅ 关键修复:计算剩余时间时,需要减去所有已恢复的暂停时间 | |||||
| const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; | ||||
| const now = new Date(); | const now = new Date(); | ||||
| // ✅ 实际工作时间 = 当前时间 - 开始时间 - 所有已恢复的暂停时间 | |||||
| const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs; | const elapsed = now.getTime() - start.getTime() - totalPausedTimeMs; | ||||
| const remaining = durationMs - elapsed; | const remaining = durationMs - elapsed; | ||||
| @@ -276,7 +302,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| }); | }); | ||||
| if (remaining <= 0) { | if (remaining <= 0) { | ||||
| // Over time - show negative time in red | |||||
| const overTime = Math.abs(remaining); | const overTime = Math.abs(remaining); | ||||
| const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); | const minutes = Math.floor(overTime / 60000).toString().padStart(2, "0"); | ||||
| const seconds = Math.floor((overTime % 60000) / 1000).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(); | update(); | ||||
| // Only set interval if not paused | |||||
| if (!isPaused) { | if (!isPaused) { | ||||
| const timer = setInterval(update, 1000); | const timer = setInterval(update, 1000); | ||||
| return () => clearInterval(timer); | return () => clearInterval(timer); | ||||
| } | } | ||||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]); | }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]); | ||||
| // Reset frozen time when status changes from paused to in progress | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused"; | const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused"; | ||||
| const isNowInProgress = lineDetail?.status === "InProgress"; | const isNowInProgress = lineDetail?.status === "InProgress"; | ||||
| if (wasPaused && isNowInProgress && frozenRemainingTime) { | 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); | setFrozenRemainingTime(null); | ||||
| } | } | ||||
| }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]); | }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]); | ||||
| const handleSubmitOutput = async () => { | const handleSubmitOutput = async () => { | ||||
| if (!lineDetail?.id) return; | if (!lineDetail?.id) return; | ||||
| try { | try { | ||||
| // 直接使用 actions.ts 中定义的函数 | |||||
| await updateProductProcessLineQty({ | await updateProductProcessLineQty({ | ||||
| productProcessLineId: lineDetail?.id || 0 as number, | productProcessLineId: lineDetail?.id || 0 as number, | ||||
| byproductName: outputData.byproductName, | byproductName: outputData.byproductName, | ||||
| @@ -324,7 +343,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| byproductUom: outputData.byproductUom, | byproductUom: outputData.byproductUom, | ||||
| outputFromProcessQty: outputData.outputFromProcessQty, | outputFromProcessQty: outputData.outputFromProcessQty, | ||||
| outputFromProcessUom: outputData.outputFromProcessUom, | outputFromProcessUom: outputData.outputFromProcessUom, | ||||
| // outputFromProcessUom: outputData.outputFromProcessUom, | |||||
| defectQty: outputData.defectQty, | defectQty: outputData.defectQty, | ||||
| defectUom: outputData.defectUom, | defectUom: outputData.defectUom, | ||||
| defect2Qty: outputData.defect2Qty, | defect2Qty: outputData.defect2Qty, | ||||
| @@ -350,12 +368,11 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| productProcessIssueStatus: detail.productProcessIssueStatus | productProcessIssueStatus: detail.productProcessIssueStatus | ||||
| }); | }); | ||||
| setLineDetail(detail as any); | setLineDetail(detail as any); | ||||
| // 初始化 outputData 从 lineDetail | |||||
| setOutputData(prev => ({ | setOutputData(prev => ({ | ||||
| ...prev, | ...prev, | ||||
| productProcessLineId: detail.id, | 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, | defectQty: detail.defectQty || 0, | ||||
| defectUom: detail.defectUom || "", | defectUom: detail.defectUom || "", | ||||
| defectDescription: detail.defectDescription || "", | defectDescription: detail.defectDescription || "", | ||||
| @@ -382,7 +399,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| } | } | ||||
| }; | }; | ||||
| // 处理 QR 码扫描效果 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { | if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { | ||||
| const latestQr = qrValues[qrValues.length - 1]; | const latestQr = qrValues[qrValues.length - 1]; | ||||
| @@ -392,20 +408,88 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| } | } | ||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | ||||
| //processQrCode(latestQr); | |||||
| } | } | ||||
| }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]); | }, [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 = () => { | const handleOpenReasonModel = () => { | ||||
| setIsOpenReasonModel(true); | setIsOpenReasonModel(true); | ||||
| setPauseReason(""); // 重置原因 | |||||
| setPauseReason(""); | |||||
| }; | }; | ||||
| const handleCloseReasonModel = () => { | const handleCloseReasonModel = () => { | ||||
| setIsOpenReasonModel(false); | setIsOpenReasonModel(false); | ||||
| setPauseReason(""); // 清空原因 | |||||
| setPauseReason(""); | |||||
| }; | }; | ||||
| const handleSaveReason = async () => { | const handleSaveReason = async () => { | ||||
| if (!pauseReason.trim()) { | if (!pauseReason.trim()) { | ||||
| @@ -421,7 +505,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| }); | }); | ||||
| setIsOpenReasonModel(false); | setIsOpenReasonModel(false); | ||||
| setPauseReason(""); | setPauseReason(""); | ||||
| // 刷新 line detail | |||||
| fetchProductProcessLineDetail(lineDetail.id) | fetchProductProcessLineDetail(lineDetail.id) | ||||
| .then((detail) => { | .then((detail) => { | ||||
| setLineDetail(detail as any); | setLineDetail(detail as any); | ||||
| @@ -435,7 +518,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| } | } | ||||
| }; | }; | ||||
| // ✅ Add this new handler for resume | |||||
| const handleResume = async () => { | const handleResume = async () => { | ||||
| if (!lineDetail?.productProcessIssueId) { | if (!lineDetail?.productProcessIssueId) { | ||||
| console.error("No productProcessIssueId found"); | console.error("No productProcessIssueId found"); | ||||
| @@ -446,13 +528,11 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| await saveProductProcessResumeTime(lineDetail.productProcessIssueId); | await saveProductProcessResumeTime(lineDetail.productProcessIssueId); | ||||
| console.log("✅ Resume API called successfully"); | console.log("✅ Resume API called successfully"); | ||||
| // ✅ Refresh line detail after resume | |||||
| if (lineDetail?.id) { | if (lineDetail?.id) { | ||||
| fetchProductProcessLineDetail(lineDetail.id) | fetchProductProcessLineDetail(lineDetail.id) | ||||
| .then((detail) => { | .then((detail) => { | ||||
| console.log("✅ Line detail refreshed after resume:", detail); | console.log("✅ Line detail refreshed after resume:", detail); | ||||
| setLineDetail(detail as any); | setLineDetail(detail as any); | ||||
| // Clear frozen time when resuming | |||||
| setFrozenRemainingTime(null); | setFrozenRemainingTime(null); | ||||
| setLastPauseTime(null); | setLastPauseTime(null); | ||||
| }) | }) | ||||
| @@ -465,6 +545,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| alert(t("Failed to resume. Please try again.")); | alert(t("Failed to resume. Please try again.")); | ||||
| } | } | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| @@ -472,18 +553,21 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| {/* 如果已完成,显示合并的视图 */} | |||||
| {processData && ( | |||||
| <OverallTimeRemainingCard processData={processData} /> | |||||
| )} | |||||
| {isCompleted ? ( | {isCompleted ? ( | ||||
| <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | <Card sx={{ bgcolor: 'success.50', border: '2px solid', borderColor: 'success.main', mb: 3 }}> | ||||
| <CardContent> | <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"> | <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 }}> | <Typography variant="h6" gutterBottom sx={{ mt: 2 }}> | ||||
| {t("Step Information")} | {t("Step Information")} | ||||
| </Typography> | </Typography> | ||||
| @@ -510,9 +594,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| {/*<Divider sx={{ my: 2 }} />*/} | |||||
| {/* 产出数据部分 */} | |||||
| <Typography variant="h6" gutterBottom sx={{ mt: 2 }}> | <Typography variant="h6" gutterBottom sx={{ mt: 2 }}> | ||||
| {t("Production Output Data")} | {t("Production Output Data")} | ||||
| </Typography> | </Typography> | ||||
| @@ -526,7 +607,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {/* Output from Process */} | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | <Typography fontWeight={500}>{t("Output from Process")}</Typography> | ||||
| @@ -539,27 +619,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </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' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | ||||
| @@ -573,7 +632,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TableCell> | <TableCell> | ||||
| <Typography>{lineDetail.defectDescription || "-"}</Typography> | <Typography>{lineDetail.defectDescription || "-"}</Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -588,7 +646,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TableCell> | <TableCell> | ||||
| <Typography>{lineDetail.defectDescription3 || "-"}</Typography> | <Typography>{lineDetail.defectDescription3 || "-"}</Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -603,9 +660,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TableCell> | <TableCell> | ||||
| <Typography>{lineDetail.defectDescription2 || "-"}</Typography> | <Typography>{lineDetail.defectDescription2 || "-"}</Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| {/* Scrap */} | |||||
| <TableRow sx={{ bgcolor: 'error.50' }}> | <TableRow sx={{ bgcolor: 'error.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | ||||
| @@ -623,8 +678,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </Card> | </Card> | ||||
| ) : ( | ) : ( | ||||
| <> | <> | ||||
| {/* 如果未完成,显示原来的两个部分 */} | |||||
| {/* 当前步骤信息 */} | |||||
| {!showOutputTable && ( | {!showOutputTable && ( | ||||
| <Grid container spacing={2} sx={{ mb: 3 }}> | <Grid container spacing={2} sx={{ mb: 3 }}> | ||||
| <Grid item xs={12} > | <Grid item xs={12} > | ||||
| @@ -654,6 +707,27 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| > | > | ||||
| {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} | {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} | ||||
| </Typography> | </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" && ( | {lineDetail?.status === "Paused" && ( | ||||
| <Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}> | <Typography variant="caption" color="warning.main" sx={{ mt: 0.5, display: 'block' }}> | ||||
| {t("Timer Paused")} | {t("Timer Paused")} | ||||
| @@ -662,17 +736,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| <Stack direction="row" spacing={2} justifyContent="center" sx={{ mt: 2 }}> | <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'? ( | { lineDetail?.status === 'InProgress'? ( | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -687,7 +750,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| variant="contained" | variant="contained" | ||||
| color="success" | color="success" | ||||
| startIcon={<PlayArrowIcon />} | startIcon={<PlayArrowIcon />} | ||||
| onClick={handleResume} // ✅ Change from inline call to handler | |||||
| onClick={handleResume} | |||||
| > | > | ||||
| {t("Continue")} | {t("Continue")} | ||||
| </Button> | </Button> | ||||
| @@ -710,8 +773,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| {/* ========== 产出输入表单 ========== */} | {/* ========== 产出输入表单 ========== */} | ||||
| {showOutputTable && ( | {showOutputTable && ( | ||||
| <Box> | <Box> | ||||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | ||||
| <Table size="small"> | <Table size="small"> | ||||
| <TableHead> | <TableHead> | ||||
| @@ -723,7 +784,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {/* start line output */} | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | <Typography fontWeight={500}>{t("Output from Process")}</Typography> | ||||
| @@ -756,8 +816,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| {/* defect 1 */} | |||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(1)")}</Typography> | <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(1)")}</Typography> | ||||
| @@ -789,7 +847,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| size="small" | size="small" | ||||
| //value={outputData.defectUom} | |||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defectDescription: e.target.value | defectDescription: e.target.value | ||||
| @@ -797,7 +854,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| {/* defect 2 */} | |||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography> | <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(2)")}</Typography> | ||||
| @@ -829,7 +885,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| size="small" | size="small" | ||||
| //value={outputData.defectUom} | |||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defectDescription2: e.target.value | defectDescription2: e.target.value | ||||
| @@ -837,7 +892,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| {/* defect 3 */} | |||||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | <TableRow sx={{ bgcolor: 'warning.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography> | <Typography fontWeight={500} color="warning.dark">{t("Defect")}{t("(3)")}</Typography> | ||||
| @@ -869,7 +923,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| size="small" | size="small" | ||||
| //value={outputData.defectUom} | |||||
| onChange={(e) => setOutputData({ | onChange={(e) => setOutputData({ | ||||
| ...outputData, | ...outputData, | ||||
| defectDescription3: e.target.value | defectDescription3: e.target.value | ||||
| @@ -877,7 +930,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| {/* scrap */} | |||||
| <TableRow sx={{ bgcolor: 'error.50' }}> | <TableRow sx={{ bgcolor: 'error.50' }}> | ||||
| <TableCell> | <TableCell> | ||||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | ||||
| @@ -909,7 +961,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </TableBody> | </TableBody> | ||||
| </Table> | </Table> | ||||
| {/* submit button */} | |||||
| <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -928,6 +979,17 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| </Paper> | </Paper> | ||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| {/* ========== Bag Consumption Form ========== */} | |||||
| {((showOutputTable || isCompleted) && shouldShowBagForm && jobOrderId && lineId) && ( | |||||
| <BagConsumptionForm | |||||
| jobOrderId={jobOrderId} | |||||
| lineId={lineId} | |||||
| bomDescription={processData?.bomDescription} | |||||
| isLastLine={shouldShowBagForm} | |||||
| onRefresh={handleRefreshLineDetail} | |||||
| /> | |||||
| )} | |||||
| </> | </> | ||||
| )} | )} | ||||
| <Dialog | <Dialog | ||||
| @@ -947,7 +1009,6 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| rows={4} | rows={4} | ||||
| value={pauseReason} | value={pauseReason} | ||||
| onChange={(e) => setPauseReason(e.target.value)} | onChange={(e) => setPauseReason(e.target.value)} | ||||
| //required | |||||
| /> | /> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -142,28 +142,25 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") { | if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") { | ||||
| setError("acceptQty", { message: t("value must be a number") }); | setError("acceptQty", { message: t("value must be a number") }); | ||||
| } else | } else | ||||
| if (!Number.isInteger(accQty)) { | |||||
| setError("acceptQty", { message: t("value must be integer") }); | |||||
| } | |||||
| if (accQty > itemDetail.acceptedQty) { | if (accQty > itemDetail.acceptedQty) { | ||||
| setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ | ||||
| itemDetail.acceptedQty}` }); | itemDetail.acceptedQty}` }); | ||||
| } else | } else | ||||
| if (accQty < 1) { | |||||
| if (accQty <= 0) { | |||||
| setError("acceptQty", { message: t("minimal value is 1") }); | setError("acceptQty", { message: t("minimal value is 1") }); | ||||
| } else | } else | ||||
| console.log("%c Validated accQty:", "color:yellow", accQty); | console.log("%c Validated accQty:", "color:yellow", accQty); | ||||
| } | } | ||||
| },[setError, qcDecision, accQty, itemDetail]) | },[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); | const qcResultItems = qcResult; //console.log("Validating:", qcResultItems); | ||||
| @@ -586,17 +583,26 @@ useEffect(() => { | |||||
| onInput={(e: React.ChangeEvent<HTMLInputElement>) => { | onInput={(e: React.ChangeEvent<HTMLInputElement>) => { | ||||
| const input = e.target.value; | const input = e.target.value; | ||||
| const numReg = /^[0-9]+$/ | |||||
| // 允许数字和小数点,但只允许一个小数点 | |||||
| const numReg = /^\d+(\.\d*)?$/ | |||||
| let r = ''; | 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 { | } else { | ||||
| r = Number(input).toString() | |||||
| r = input; | |||||
| } | } | ||||
| e.target.value = r; | e.target.value = r; | ||||
| }} | }} | ||||
| inputProps={{ min: 1, max:itemDetail.acceptedQty }} | |||||
| inputProps={{ min: 0.01, max:itemDetail.acceptedQty, step: 0.01 }} | |||||
| // onChange={(e) => { | // onChange={(e) => { | ||||
| // const inputValue = e.target.value; | // const inputValue = e.target.value; | ||||
| // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | ||||