From f75796a8dbc1b52b4832e54e0587ab9a824fc366 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 30 Dec 2025 14:05:07 +0800 Subject: [PATCH] bag and time --- src/app/api/bag/action.ts | 47 +++ src/app/api/jo/actions.ts | 43 +++ src/app/api/jo/index.ts | 1 + src/components/JoSearch/JoCreateFormModal.tsx | 243 ++++++++----- src/components/JoSearch/JoSearch.tsx | 230 ++++++++++-- .../PickOrderSearch/PickExecution.tsx | 26 -- .../ProductionProcess/BagConsumptionForm.tsx | 309 ++++++++++++++++ .../OverallTimeRemainingCard.tsx | 206 +++++++++++ .../ProcessSummaryHeader.tsx | 55 ++- .../ProductionProcessDetail.tsx | 343 +++++++----------- .../ProductionProcessJobOrderDetail.tsx | 83 ++++- .../ProductionProcessList.tsx | 25 +- .../ProductionProcessStepExecution.tsx | 297 +++++++++------ src/components/Qc/QcComponent.tsx | 42 ++- 14 files changed, 1449 insertions(+), 501 deletions(-) create mode 100644 src/app/api/bag/action.ts create mode 100644 src/components/ProductionProcess/BagConsumptionForm.tsx create mode 100644 src/components/ProductionProcess/OverallTimeRemainingCard.tsx diff --git a/src/app/api/bag/action.ts b/src/app/api/bag/action.ts new file mode 100644 index 0000000..0232168 --- /dev/null +++ b/src/app/api/bag/action.ts @@ -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( + `${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( + `${BASE_API_URL}/bag/createJoBagConsumption`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + } + ); + }); \ No newline at end of file diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index c291f92..306e1f4 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -16,6 +16,7 @@ export interface SaveJo { type: string; //jobType?: string; jobTypeId?: number; + productionPriority?: number; } export interface SaveJoResponse { @@ -246,6 +247,7 @@ export interface ProductProcessWithLinesResponse { jobOrderId?: number; jobOrderCode: string; jobOrderStatus: string; + bomDescription: string; jobType: string; isDark: string; isDense: number; @@ -321,6 +323,7 @@ export interface AllJoborderProductProcessInfoResponse { date: string; matchStatus: string; bomId?: number; + productionPriority: number; assignedTo: number; pickOrderId: number; pickOrderStatus: string; @@ -328,6 +331,7 @@ export interface AllJoborderProductProcessInfoResponse { itemName: string; requiredQty: number; jobOrderId: number; + timeNeedToComplete: number; uom: string; stockInLineId: number; jobOrderCode: string; @@ -578,6 +582,11 @@ export interface JobOrderListForPrintQrCodeResponse { stockOutLineStatus: string; finihedTime: string; } +export interface UpdateJoPlanStartRequest { + id: number; + planStart: string; // Format: YYYY-MM-DDTHH:mm:ss or YYYY-MM-DD +} + export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { return serverFetchJson( `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, @@ -1086,4 +1095,38 @@ export const fetchFGStockInLabel = async (data: ExportFGStockInLabelRequest): Pr ); return reportBlob; +}; +export const updateJoPlanStart = cache(async (data: UpdateJoPlanStartRequest) => { + return serverFetchJson(`${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( + `${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( + `${BASE_API_URL}/product-process/Demo/ProcessLine/pass/${lineId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + } + ); }; \ No newline at end of file diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts index 76c1d59..6ce2e51 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -32,6 +32,7 @@ export interface JobOrder { jobTypeName: string; sufficientCount: number; insufficientCount: number; + productionPriority: number; // TODO pack below into StockInLineInfo stockInLineId?: number; stockInLineStatus?: string; diff --git a/src/components/JoSearch/JoCreateFormModal.tsx b/src/components/JoSearch/JoCreateFormModal.tsx index f8d8103..2d58cb7 100644 --- a/src/components/JoSearch/JoCreateFormModal.tsx +++ b/src/components/JoSearch/JoCreateFormModal.tsx @@ -8,7 +8,7 @@ import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pi import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs, { Dayjs } from "dayjs"; import { isFinite } from "lodash"; -import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo } from "react"; +import React, { SetStateAction, SyntheticEvent, useCallback, useEffect, useMemo, useState} from "react"; import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { msg } from "../Swal/CustomAlerts"; @@ -30,17 +30,52 @@ const JoCreateFormModal: React.FC = ({ onSearch, }) => { const { t } = useTranslation("jo"); + const [multiplier, setMultiplier] = useState(1); const formProps = useForm({ mode: "onChange", + defaultValues: { + productionPriority: 50 + } }); const { reset, trigger, watch, control, register, formState: { errors }, setValue } = formProps // 监听 bomId 变化 const selectedBomId = watch("bomId"); - + /* + const handleAutoCompleteChange = useCallback( + (event: SyntheticEvent, value: BomCombo, onChange: (...event: any[]) => void) => { + console.log("BOM changed to:", value); + onChange(value.id); + + // 重置倍数为 1 + setMultiplier(1); + + // 1) 根据 BOM 设置数量(倍数 * outputQty) + if (value.outputQty != null) { + const calculatedQty = 1 * Number(value.outputQty); + formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true }); + } + + // 2) 选 BOM 时,把日期默认设为"今天" + const today = dayjs(); + const todayStr = dayjsToDateString(today, "input"); + formProps.setValue("planStart", todayStr, { shouldValidate: true, shouldDirty: true }); + }, + [formProps] + ); + */ + // 添加 useEffect 来监听倍数变化,自动计算 reqQty + useEffect(() => { + const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); + if (selectedBom && selectedBom.outputQty != null) { + const calculatedQty = multiplier * Number(selectedBom.outputQty); + formProps.setValue("reqQty", calculatedQty, { shouldValidate: true, shouldDirty: true }); + } + }, [multiplier, selectedBomId, bomCombo, formProps]); const onModalClose = useCallback(() => { reset() onClose() + setMultiplier(1); }, [reset, onClose]) const handleAutoCompleteChange = useCallback( @@ -61,65 +96,7 @@ const JoCreateFormModal: React.FC = ({ [formProps] ); - // 使用 useMemo 来计算过滤后的 jobTypes,响应 selectedBomId 变化 - /* - const filteredJobTypes = useMemo(() => { - console.log("getFilteredJobTypes called, selectedBomId:", selectedBomId); - - if (!selectedBomId) { - console.log("No BOM selected, returning all jobTypes:", jobTypes); - return jobTypes; - } - - const selectedBom = bomCombo.find(bom => bom.id === selectedBomId); - console.log("Selected BOM:", selectedBom); - console.log("Selected BOM full object:", JSON.stringify(selectedBom, null, 2)); - - if (!selectedBom) { - console.log("BOM not found, returning all jobTypes"); - return jobTypes; - } - - // 检查 description 是否存在 - const description = selectedBom.description; - console.log("BOM description (raw):", description); - console.log("BOM description type:", typeof description); - console.log("BOM description is undefined?", description === undefined); - console.log("BOM description is null?", description === null); - - if (!description) { - console.log("BOM description is missing or empty, returning all jobTypes"); - return jobTypes; - } - - const descriptionUpper = description.toUpperCase(); - console.log("BOM description (uppercase):", descriptionUpper); - console.log("All jobTypes:", jobTypes); - - let filtered: JobTypeResponse[] = []; - - if (descriptionUpper === "WIP") { - filtered = jobTypes.filter(jt => { - const jobTypeName = jt.name.toUpperCase(); - const shouldInclude = jobTypeName !== "FG"; - console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); - return shouldInclude; - }); - } else if (descriptionUpper === "FG") { - filtered = jobTypes.filter(jt => { - const jobTypeName = jt.name.toUpperCase(); - const shouldInclude = jobTypeName !== "WIP"; - console.log(`JobType ${jt.name} (${jobTypeName}): ${shouldInclude ? "included" : "excluded"}`); - return shouldInclude; - }); - } else { - filtered = jobTypes; - } - - console.log("Filtered jobTypes:", filtered); - return filtered; - }, [bomCombo, jobTypes, selectedBomId]); -*/ + // 当 BOM 改变时,自动选择匹配的 Job Type useEffect(() => { if (!selectedBomId) { @@ -174,6 +151,10 @@ const JoCreateFormModal: React.FC = ({ data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day')) } data.jobTypeId = Number(data.jobTypeId); + // 如果 productionPriority 为空或无效,使用默认值 50 + data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority) + ? Number(data.productionPriority) + : 50; const response = await manualCreateJo(data) if (response) { onSearch(); @@ -283,31 +264,73 @@ const JoCreateFormModal: React.FC = ({ render={({ field, fieldState: { error } }) => { const selectedBom = bomCombo.find(bom => bom.id === formProps.watch("bomId")); const uom = selectedBom?.outputQtyUom || ""; + const outputQty = selectedBom?.outputQty ?? 0; + const calculatedValue = multiplier * outputQty; return ( - { - const val = e.target.value === "" ? undefined : Number(e.target.value); - field.onChange(val); - }} - InputProps={{ - endAdornment: uom ? ( - - - {uom} - - - ) : null - }} - /> + + + + {uom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + + + × + + { + 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 }} + /> + + = + + + + {uom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + ); }} /> @@ -349,6 +372,58 @@ const JoCreateFormModal: React.FC = ({ }} /> + + { + if (value === undefined || value === null || isNaN(value)) { + return t("Production Priority required!") as string; + } + return true; + } + }} + render={({ field, fieldState: { error } }) => ( + { + const inputValue = e.target.value; + // 允许空字符串(用户正在删除) + if (inputValue === "") { + field.onChange(""); + return; + } + // 转换为数字并验证范围 + const numValue = Number(inputValue); + if (!isNaN(numValue) && numValue >= 1 && numValue <= 100) { + field.onChange(numValue); + } + }} + /> + )} + /> + = ({ defaultInputs, bomCombo, printerCombo, jobT const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); - + const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); + const [operationPriority, setOperationPriority] = useState(50); + const [selectedJo, setSelectedJo] = useState(null); + const [selectedProductProcessId, setSelectedProductProcessId] = useState(null); + const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); + const [planStartDate, setPlanStartDate] = useState(null); + const [selectedJoForDate, setSelectedJoForDate] = useState(null); + const fetchJoDetailClient = async (id: number): Promise => { const response = await fetch(`/api/jo/detail?id=${id}`); if (!response.ok) { @@ -98,6 +110,32 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchInventoryData(); }, []); + + const handleOpenPriorityDialog = useCallback(async (jo: JobOrder) => { + setSelectedJo(jo); + setOperationPriority(jo.productionPriority ?? 50); + + // 获取 productProcessId + try { + const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions"); + const processes = await fetchProductProcessesByJobOrderId(jo.id); + if (processes && processes.length > 0) { + setSelectedProductProcessId(processes[0].id); + setOpenOperationPriorityDialog(true); + } else { + msg(t("No product process found for this job order")); + } + } catch (error) { + console.error("Error fetching product process:", error); + msg(t("Error loading product process")); + } + }, [t]); + + const handleClosePriorityDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { + setOpenOperationPriorityDialog(false); + setSelectedJo(null); + setSelectedProductProcessId(null); + }, []); const getStockAvailable = (pickLine: JoDetailPickLine) => { const inventory = inventoryData.find(inventory => @@ -137,14 +175,71 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT options: jobTypes.map(jt => jt.name) }, ], [t, jobTypes]) - + const handleOpenPlanStartDialog = useCallback((jo: JobOrder) => { + setSelectedJoForDate(jo); + // 将 planStart 数组转换为 dayjs 对象 + if (jo.planStart && Array.isArray(jo.planStart)) { + setPlanStartDate(arrayToDayjs(jo.planStart)); + } else { + setPlanStartDate(dayjs()); + } + setOpenPlanStartDialog(true); + }, []); + const columns = useMemo[]>( () => [ + { + name: "planStart", + label: t("Estimated Production Date"), + align: "left", + headerAlign: "left", + renderCell: (row) => { + return ( + + {row.planStart ? arrayToDateString(row.planStart) : '-'} + {row.status == "planning" && ( + { + e.stopPropagation(); + handleOpenPlanStartDialog(row); + }} + sx={{ padding: '4px' }} + > + + + )} + + ); + } + }, + { + name: "productionPriority", + label: t("Production Priority"), + renderCell: (row) => { + return ( + + {integerFormatter.format(row.productionPriority)} + { + e.stopPropagation(); + handleOpenPriorityDialog(row); + }} + sx={{ padding: '4px' }} + > + + + + ); + } + }, { name: "code", label: t("Code"), flex: 2 }, + { name: "item", label: `${t("Item Name")}`, @@ -170,23 +265,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT return row.item?.uom ? t(row.item.uom.udfudesc) : '-' } }, - { - name: "status", - label: t("Status"), - renderCell: (row) => { - return - {t(upperFirst(row.status))} - - } - },{ - name: "planStart", - label: t("Estimated Production Date"), - align: "left", - headerAlign: "left", - renderCell: (row) => { - return row.planStart ? arrayToDateString(row.planStart) : '-' - } - }, { name: "stockStatus" as keyof JobOrder, label: t("BOM Status"), @@ -201,6 +279,15 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT ); } }, + { + name: "status", + label: t("Status"), + renderCell: (row) => { + return + {t(upperFirst(row.status))} + + } + }, { name: "jobTypeName", label: t("Job Type"), @@ -226,7 +313,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT ) } }, - ], [t, inventoryData, detailedJos] + ], [t, inventoryData, detailedJos, handleOpenPriorityDialog,handleOpenPlanStartDialog] ) // 按照 PoSearch 的模式:创建 newPageFetch 函数 @@ -256,7 +343,20 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT }, [], ); - + const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { + const response = await updateProductProcessPriority(productProcessId, productionPriority) + if (response) { + // 刷新数据 + await newPageFetch(pagingController, inputs); + } + }, [pagingController, inputs, newPageFetch]); + const handleConfirmPriority = useCallback(async () => { + if (!selectedProductProcessId) return; + await handleUpdateOperationPriority(selectedProductProcessId, Number(operationPriority)); + setOpenOperationPriorityDialog(false); + setSelectedJo(null); + setSelectedProductProcessId(null); + }, [selectedProductProcessId, operationPriority, handleUpdateOperationPriority]); // 按照 PoSearch 的模式:使用相同的 useEffect 逻辑 useEffect(() => { newPageFetch(pagingController, inputs); @@ -352,6 +452,31 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT setPagingController(defaultPagingController); }, [defaultInputs]) + const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { + setOpenPlanStartDialog(false); + setSelectedJoForDate(null); + setPlanStartDate(null); + }, []); + + const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { + const response = await updateJoPlanStart({ id: jobOrderId, planStart }); + if (response) { + // 刷新数据 + await newPageFetch(pagingController, inputs); + } + }, [pagingController, inputs, newPageFetch]); + + const handleConfirmPlanStart = useCallback(async () => { + if (!selectedJoForDate?.id || !planStartDate) return; + + // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss) + const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`; + await handleUpdatePlanStart(selectedJoForDate.id, dateString); + setOpenPlanStartDialog(false); + setSelectedJoForDate(null); + setPlanStartDate(null); + }, [selectedJoForDate, planStartDate, handleUpdatePlanStart]); + const onReset = useCallback(() => { setInputs(defaultInputs); @@ -413,7 +538,66 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT inputDetail={modalInfo} printerCombo={printerCombo} /> + + {t("Update Production Priority")} + + setOperationPriority(Number(e.target.value))} + /> + + + + + + + + {t("Update Estimated Production Date")} + + + setPlanStartDate(newValue)} + slotProps={{ + textField: { + fullWidth: true, + margin: "dense", + autoFocus: true, + } + }} + /> + + + + + + + + + } export default JoSearch; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index b3dbcc3..486b52e 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -203,32 +203,6 @@ const PickExecution: React.FC = ({ filterArgs }) => { fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); }, [fetchNewPageConsoPickOrder, filterArgs]); - const handleUpdateStockOutLineStatus = useCallback(async ( - stockOutLineId: number, - status: string, - qty?: number - ) => { - try { - const updateData = { - id: stockOutLineId, - status: status, - qty: qty - }; - - console.log("Updating stock out line status:", updateData); - const result = await updateStockOutLineStatus(updateData); - - if (result) { - console.log("Stock out line status updated successfully:", result); - - if (selectedRowId) { - handleRowSelect(selectedRowId); - } - } - } catch (error) { - console.error("Error updating stock out line status:", error); - } - }, [selectedRowId]); const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { let isReleasable = true; diff --git a/src/components/ProductionProcess/BagConsumptionForm.tsx b/src/components/ProductionProcess/BagConsumptionForm.tsx new file mode 100644 index 0000000..b98a7a2 --- /dev/null +++ b/src/components/ProductionProcess/BagConsumptionForm.tsx @@ -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 = ({ + jobOrderId, + lineId, + bomDescription, + isLastLine, + onRefresh, +}) => { + const { t } = useTranslation(["common", "jo"]); + const [bagList, setBagList] = useState([]); + const [bagConsumptionRows, setBagConsumptionRows] = useState([ + { 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 ( + + + + {t("Bag Consumption")} + + + {isLoadingBags ? ( + + + + ) : ( + <> + + + + {t("Bag")} + + {t("Consumed Qty")} + + + {t("Scrap Qty")} + + + {t("Action")} + + + + + {bagConsumptionRows.map((row, index) => ( + + + + + + + handleBagRowChange( + index, + "consumedQty", + Number(e.target.value) || 0 + ) + } + inputProps={{ min: 0 }} + /> + + + + handleBagRowChange( + index, + "scrapQty", + Number(e.target.value) || 0 + ) + } + inputProps={{ min: 0 }} + /> + + + {bagConsumptionRows.length > 1 && ( + handleDeleteBagRow(index)} + > + + + )} + + + ))} + +
+ + + + + + + )} +
+
+ ); +}; + +export default BagConsumptionForm; \ No newline at end of file diff --git a/src/components/ProductionProcess/OverallTimeRemainingCard.tsx b/src/components/ProductionProcess/OverallTimeRemainingCard.tsx new file mode 100644 index 0000000..7b06f68 --- /dev/null +++ b/src/components/ProductionProcess/OverallTimeRemainingCard.tsx @@ -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 = ({ + processData, +}) => { + const { t } = useTranslation(["common", "jo"]); + const [overallRemainingTime, setOverallRemainingTime] = useState(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 ( + + + + {t("Overall Time Remaining")} + + + + {isOverTime ? `${t("Over Time")}: ${overallRemainingTime}` : overallRemainingTime} + + + + + ); +}; + +export default OverallTimeRemainingCard; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProcessSummaryHeader.tsx b/src/components/ProductionProcess/ProcessSummaryHeader.tsx index 8412ac9..e153375 100644 --- a/src/components/ProductionProcess/ProcessSummaryHeader.tsx +++ b/src/components/ProductionProcess/ProcessSummaryHeader.tsx @@ -2,21 +2,29 @@ import { Card, CardContent, Stack, Typography } from "@mui/material"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { useTranslation } from "react-i18next"; +import { ProductProcessWithLinesResponse } from "@/app/api/jo/actions"; interface Props { - processData?: { - jobOrderCode?: string; - itemCode?: string; - itemName?: string; - jobType?: string; - outputQty?: number | string; - outputQtyUom?: string; - date?: string; - }; + processData?: ProductProcessWithLinesResponse | null; } const ProcessSummaryHeader: React.FC = ({ 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 ( @@ -38,6 +46,33 @@ const ProcessSummaryHeader: React.FC = ({ processData }) => { {t("Production Date")}: {processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} + + {t("Assume Time Need")}: {assumeTimeNeed} {t("minutes")} + + + {t("Start Time")}: + {startTime + ? ( + <> + {dayjs(startTime).format("MM-DD")} + {" "} + {dayjs(startTime).format("HH:mm")} + + ) + : "-"} + + + + {t("Assume End Time")}: + {assumeEndTime ? ( + <> + {assumeEndTime.format("MM-DD")} + {" "} + {assumeEndTime.format("HH:mm")} + + ) : "-"} + + diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index e972450..0095983 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -39,8 +39,13 @@ import { JobOrderProcessLineDetailResponse, ProductProcessLineInfoResponse, startProductProcessLine, - fetchProductProcessesByJobOrderId + fetchProductProcessesByJobOrderId, + ProductProcessWithLinesResponse, // ✅ 添加 + ProductProcessLineResponse, + passProductProcessLine, } from "@/app/api/jo/actions"; + import { updateProductProcessLineStatus } from "@/app/api/jo/actions"; + import { fetchNameList, NameList } from "@/app/api/user/actions"; import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; import ProductionOutputFormPage from "./ProductionOutputFormPage"; @@ -62,8 +67,8 @@ const ProductionProcessDetail: React.FC = ({ const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [showOutputPage, setShowOutputPage] = useState(false); // 基本信息 - const [processData, setProcessData] = useState(null); - const [lines, setLines] = useState([]); + const [processData, setProcessData] = useState(null); // ✅ 修改类型 + const [lines, setLines] = useState([]); // ✅ 修改类型 const [loading, setLoading] = useState(false); // 选中的 line 和执行状态 @@ -125,7 +130,7 @@ const ProductionProcessDetail: React.FC = ({ setProcessData(currentProcess); // 使用 productProcessLines 字段(API 返回的字段名) - const lines = (currentProcess as any).productProcessLines || []; + const lines = currentProcess.productProcessLines || []; setLines(lines); console.log(" Process data loaded:", currentProcess); @@ -143,105 +148,27 @@ const ProductionProcessDetail: React.FC = ({ fetchProcessDetail(); }, [fetchProcessDetail]); - // 开始执行某个 line - - - // 提交产出数据 - /* - const processQrCode = useCallback((qrValue: string, lineId: number) => { - // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) - if (qrValue.match(/\{2fitestu(\d+)\}/)) { - const match = qrValue.match(/\{2fitestu(\d+)\}/); - const userId = parseInt(match![1]); - - fetchNameList().then((users: NameList[]) => { - const user = users.find((u: NameList) => u.id === userId); - if (user) { - setScannedOperatorId(user.id); - } - }); - return; - } - - // 设备格式:{2fiteste1} - 键盘模拟输入(测试用) - if (qrValue.match(/\{2fiteste(\d+)\}/)) { - const match = qrValue.match(/\{2fiteste(\d+)\}/); - const equipmentId = parseInt(match![1]); - setScannedEquipmentId(equipmentId); - return; - } - - - // 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1" - const trimmedValue = qrValue.trim(); - - // 检查 operatorId 格式 - const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i); - if (operatorMatch) { - const operatorId = parseInt(operatorMatch[1]); - fetchNameList().then((users: NameList[]) => { - const user = users.find((u: NameList) => u.id === operatorId); - if (user) { - setScannedOperatorId(user.id); - } else { - console.warn(`User with ID ${operatorId} not found`); - } - }); - return; - } - - // 检查 equipmentId 格式 - const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i); - if (equipmentMatch) { - const equipmentId = parseInt(equipmentMatch[1]); - setScannedEquipmentId(equipmentId); - return; - } - - // 其他格式处理(JSON、普通文本等) + const handlePassLine = useCallback(async (lineId: number) => { try { - const qrData = JSON.parse(qrValue); - // TODO: 处理 JSON 格式的 QR 码 - } catch { - // 普通文本格式 - // TODO: 处理普通文本格式 + await passProductProcessLine(lineId); + // 刷新数据 + await fetchProcessDetail(); + } catch (error) { + console.error("Error passing line:", error); + alert(t("Failed to pass line. Please try again.")); } - }, []); -*/ + }, [fetchProcessDetail, t]); + // 提交产出数据 const processQrCode = useCallback((qrValue: string, lineId: number) => { - // 设备快捷格式:{2fiteste数字} - 自动生成 equipmentTypeSubTypeEquipmentNo - // 格式:{2fiteste数字} = line.equipment_name + "-数字號" - // 例如:{2fiteste1} = "包裝機類-真空八爪魚機-1號" - if (qrValue.match(/\{2fiteste(\d+)\}/)) { - const match = qrValue.match(/\{2fiteste(\d+)\}/); - const equipmentNo = parseInt(match![1]); - - // 根据 lineId 找到对应的 line - const currentLine = lines.find(l => l.id === lineId); - if (currentLine && currentLine.equipment_name) { - const equipmentTypeSubTypeEquipmentNo = `${currentLine.equipment_name}-${equipmentNo}號`; - setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); - console.log(`Generated equipmentTypeSubTypeEquipmentNo: ${equipmentTypeSubTypeEquipmentNo}`); - } else { - // 如果找不到 line,尝试从 API 获取 line detail - console.warn(`Line with ID ${lineId} not found in current lines, fetching from API...`); - fetchProductProcessLineDetail(lineId) - .then((lineDetail) => { - // 从 lineDetail 中获取 equipment_name - const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; - if (equipmentName) { - const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; - setScannedEquipmentCode(equipmentTypeSubTypeEquipmentNo); - console.log(`Generated equipmentTypeSubTypeEquipmentNo from API: ${equipmentTypeSubTypeEquipmentNo}`); - } else { - console.warn(`Equipment name not found in line detail for lineId: ${lineId}`); - } - }) - .catch((err) => { - console.error(`Failed to fetch line detail for lineId ${lineId}:`, err); - }); - } + // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 + // 格式:{2fitesteXXX} = equipmentCode: "XXX" + // 例如:{2fiteste包裝機類-真空八爪魚機-1號} = equipmentCode: "包裝機類-真空八爪魚機-1號" + if (qrValue.match(/\{2fiteste(.+)\}/)) { + const match = qrValue.match(/\{2fiteste(.+)\}/); + const equipmentCode = match![1]; + setScannedEquipmentCode(equipmentCode); + console.log(`Set equipmentCode from shortcut: ${equipmentCode}`); return; } @@ -334,7 +261,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { const effectiveEquipmentCode = scannedEquipmentCode ?? null; - + console.log("Submitting scan data with equipmentCode:", { productProcessLineId: lineId, staffNo: scannedStaffNo, @@ -538,53 +465,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { return ( - {/* - - - - - - - - {t("Production Process Information")} - - - - {t("Job Order Code")}: {processData?.jobOrderCode} - - - {t("Is Dark")}: {processData?.isDark} - - - {t("Is Dense")}: {processData?.isDense} - - - {t("Is Float")}: {processData?.isFloat} - - - {t("Output Qty")}: {processData?.outputQty+" "+"("+processData?.outputQtyUom +")"} - - - {t("Status")}:{" "} - - - - {t("Date")}: {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)} - - - {t("Total Steps")}: {lines.length} - - - -*/} {/* ========== 第二部分:Process Lines ========== */} @@ -602,25 +482,12 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { {t("Description")} {t("EquipmentType-EquipmentName-Code")} {t("Operator")} - {/*} - {t("Processing Time (mins)")} - {t("Setup Time (mins)")} - {t("Changeover Time (mins)")} -*/} - + {t("Assume End Time")} {t("Time Information(mins)")} - {/* - - {t("Processing Time")}- - - - {t("Setup Time")} - {t("Changeover Time")} - - */} {t("Status")} @@ -638,7 +505,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; const isPaused = statusLower === 'paused'; const isPending = statusLower === 'pending' || status === ''; - + const isPass = statusLower === 'pass'; + const isPassDisabled = isCompleted || isPass; return ( {line.seqNo} @@ -648,12 +516,16 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { {line.description || "-"} {line.equipmentDetailCode||equipmentName} {line.operatorName} - {/* - {line.durationInMinutes} - {line.prepTimeInMinutes} - {line.postProdTimeInMinutes} - */} + + {line.startTime && line.durationInMinutes + ? dayjs(line.startTime) + .add(line.durationInMinutes, 'minute') + .format('MM-DD HH:mm') + : '-'} + + + {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("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} @@ -689,53 +560,92 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { ) : isPaused ? ( + ) : isPass ? ( + ) : ( - )} + ) + } {!fromJosave&&( - - {statusLower === 'pending' ? ( - - ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( - - ) : ( - - )} - - )} - - ); - })} - + + + {statusLower === 'pending' ? ( + <> + + + + ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( + <> + + + + ) : ( + <> + + + + )} + + + )} + + ); + })} + ) : ( @@ -743,13 +653,9 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { { - // setIsExecutingLine(false) - // setSelectedLineId(null) - //}} - //onOutputSubmitted={async () => { - // await fetchProcessDetail() - //}} + processData={processData} // ✅ 添加 + allLines={lines} // ✅ 添加 + jobOrderId={jobOrderId} // ✅ 添加 /> )} @@ -778,7 +684,6 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { - {/* ✅ Show both options */} {scannedEquipmentCode ? `${t("Equipment Code")}: ${scannedEquipmentCode}` : t("Please scan equipment code") diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 1e1ca0b..802dfa0 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -23,7 +23,7 @@ import { } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; -import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority} from "@/app/api/jo/actions"; +import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; @@ -38,6 +38,9 @@ import { releaseJo, startJo } from "@/app/api/jo/actions"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; import ProcessSummaryHeader from "./ProcessSummaryHeader"; import EditIcon from "@mui/icons-material/Edit"; +import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { dayjsToDateString } from "@/app/utils/formatUtil"; interface JobOrderLine { id: number; @@ -74,6 +77,9 @@ const ProductionProcessJobOrderDetail: React.FC(null); const [operationPriority, setOperationPriority] = useState(50); const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); + const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); + const [planStartDate, setPlanStartDate] = useState(null); + const fetchData = useCallback(async () => { setLoading(true); @@ -126,6 +132,38 @@ const getStockAvailable = (line: JobOrderLine) => { } return line.stockQty || 0; }; +const handleOpenPlanStartDialog = useCallback(() => { + // 将 processData.date 转换为 dayjs 对象 + if (processData?.date) { + // processData.date 可能是字符串或 Date 对象 + setPlanStartDate(dayjs(processData.date)); + } else { + setPlanStartDate(dayjs()); + } + setOpenPlanStartDialog(true); +}, [processData?.date]); + +const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { + setOpenPlanStartDialog(false); + setPlanStartDate(null); +}, []); + +const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { + const response = await updateJoPlanStart({ id: jobOrderId, planStart }); + if (response) { + await fetchData(); + } +}, [fetchData]); + +const handleConfirmPlanStart = useCallback(async () => { + if (!jobOrderId || !planStartDate) return; + + // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss) + const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`; + await handleUpdatePlanStart(jobOrderId, dateString); + setOpenPlanStartDialog(false); + setPlanStartDate(null); +}, [jobOrderId, planStartDate, handleUpdatePlanStart]); const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { const response = await updateProductProcessPriority(productProcessId, productionPriority) if (response) { @@ -273,6 +311,15 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { label={t("Target Production Date")} fullWidth disabled={true} + InputProps={{ + endAdornment: (processData?.jobOrderStatus === "planning" ? ( + + + + + + ) : null), + }} />
@@ -600,6 +647,40 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { + + {t("Update Target Production Date")} + + + setPlanStartDate(newValue)} + slotProps={{ + textField: { + fullWidth: true, + margin: "dense", + autoFocus: true, + } + }} + /> + + + + + + + diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index d52128d..aed2342 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -212,9 +212,11 @@ const ProductProcessList: React.FC = ({ onSelectProcess = ({ onSelectProcess {t("Item Name")}: {process.itemCode} {process.itemName} - {t("Required Qty")}: {process.requiredQty} {process.uom} + {t("Production Priority")}: {process.productionPriority} + + + {t("Required Qty")}: {process.requiredQty} ({process.uom}) {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"} + + {t("Assume Time Need")}: {process.timeNeedToComplete} {t("minutes")} + {statusLower !== "pending" && linesWithStatus.length > 0 && ( @@ -261,6 +269,19 @@ const ProductProcessList: React.FC = ({ onSelectProcess )} )} + {statusLower == "pending" && ( + + + {t("t")} + + + + {""} + + + + )} + diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index f0d3cd1..c65ce7d 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -27,24 +27,42 @@ import StopIcon from "@mui/icons-material/Stop"; import PauseIcon from "@mui/icons-material/Pause"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { useTranslation } from "react-i18next"; -import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest,saveProductProcessResumeTime,saveProductProcessIssueTime} from "@/app/api/jo/actions"; +import { + JobOrderProcessLineDetailResponse, + updateProductProcessLineQty, + updateProductProcessLineQrscan, + fetchProductProcessLineDetail, + UpdateProductProcessLineQtyRequest, + saveProductProcessResumeTime, + saveProductProcessIssueTime, + ProductProcessWithLinesResponse, // ✅ 添加 + ProductProcessLineResponse, // ✅ 添加 +} from "@/app/api/jo/actions"; import { Operator, Machine } from "@/app/api/jo"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; // ✅ 添加 useMemo import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { fetchNameList, NameList } from "@/app/api/user/actions"; +import BagConsumptionForm from "./BagConsumptionForm"; // ✅ 添加导入 +import OverallTimeRemainingCard from "./OverallTimeRemainingCard"; // ✅ 添加导入 +import dayjs from "dayjs"; interface ProductionProcessStepExecutionProps { - lineId: number | null - onBack: () => void - //onClose: () => void - // onOutputSubmitted: () => Promise + lineId: number | null; + onBack: () => void; + processData?: ProductProcessWithLinesResponse | null; // ✅ 添加 + allLines?: ProductProcessLineResponse[]; // ✅ 添加 + jobOrderId?: number; // ✅ 添加 } + const ProductionProcessStepExecution: React.FC = ({ lineId, onBack, + processData, // ✅ 添加 + allLines, // ✅ 添加 + jobOrderId, // ✅ 添加 }) => { const { t } = useTranslation( ["common","jo"]); const [lineDetail, setLineDetail] = useState(null); - const isCompleted = lineDetail?.status === "Completed"; + const isCompleted = lineDetail?.status === "Completed" || lineDetail?.status === "Pass"; const [outputData, setOutputData] = useState(null); const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); const [pauseReason, setPauseReason] = useState(""); - // 检查是否两个都已扫描 - //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; - - + // ✅ 添加:判断是否显示 Bag 表单的条件 + const shouldShowBagForm = useMemo(() => { + if (!processData || !allLines || !lineDetail) return false; + + // 检查 BOM description 是否为 "FG" + const bomDescription = processData.bomDescription; + if (bomDescription !== "FG") return false; + + // 检查是否是最后一个 process line(按 seqNo 排序) + const sortedLines = [...allLines].sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0)); + const maxSeqNo = sortedLines[sortedLines.length - 1]?.seqNo; + const isLastLine = lineDetail.seqNo === maxSeqNo; + + return isLastLine; + }, [processData, allLines, lineDetail]); + + // ✅ 添加:刷新 line detail 的函数 + const handleRefreshLineDetail = useCallback(async () => { + if (lineId) { + try { + const detail = await fetchProductProcessLineDetail(lineId); + setLineDetail(detail as any); + } catch (error) { + console.error("Failed to refresh line detail", error); + } + } + }, [lineId]); useEffect(() => { if (!lineId) { @@ -108,8 +149,8 @@ const ProductionProcessStepExecution: React.FC ({ ...prev, productProcessLineId: detail.id, - outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言 - outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言 + outputFromProcessQty: (detail as any).outputFromProcessQty || 0, + outputFromProcessUom: (detail as any).outputFromProcessUom || "", defectQty: detail.defectQty || 0, defectUom: detail.defectUom || "", scrapQty: detail.scrapQty || 0, @@ -124,16 +165,16 @@ const ProductionProcessStepExecution: React.FC { // Don't show time remaining if completed - if (lineDetail?.status === "Completed") { + if (lineDetail?.status === "Completed" || lineDetail?.status === "Pass") { console.log("Line is completed"); setRemainingTime(null); setIsOverTime(false); return; } - // ✅ 问题1:添加详细的调试打印 console.log("🔍 Time Remaining Debug:", { lineId: lineDetail?.id, equipmentId: lineDetail?.equipmentId, @@ -159,11 +200,9 @@ const ProductionProcessStepExecution: React.FC { if (!stopTime) return null; @@ -198,20 +234,15 @@ const ProductionProcessStepExecution: React.FC { if (isPaused) { - // If paused, freeze the time at the last calculated value - // If we don't have a frozen value yet, calculate it based on stopTime if (!frozenRemainingTime) { - // ✅ 修复问题2:正确处理 stopTime 的类型(string | number[]) const pauseTime = lineDetail.stopTime ? parseStopTime(lineDetail.stopTime) : null; - // 如果没有 stopTime,使用当前时间(首次暂停时) const pauseTimeToUse = pauseTime && !isNaN(pauseTime.getTime()) ? pauseTime : new Date(); - // ✅ 计算总暂停时间(所有已恢复的暂停记录) const totalPausedTimeMs = (lineDetail as any).totalPausedTimeMs || 0; console.log("⏸️ Paused - calculating frozen time:", { @@ -221,7 +252,6 @@ const ProductionProcessStepExecution: React.FC clearInterval(timer); } }, [lineDetail?.durationInMinutes, lineDetail?.startTime, lineDetail?.status, lineDetail?.productProcessIssueStatus, lineDetail?.stopTime, frozenRemainingTime]); - // Reset frozen time when status changes from paused to in progress useEffect(() => { const wasPaused = lineDetail?.status === "Paused" || lineDetail?.productProcessIssueStatus === "Paused"; const isNowInProgress = lineDetail?.status === "InProgress"; if (wasPaused && isNowInProgress && frozenRemainingTime) { - // When resuming, we need to account for the pause duration - // For now, we'll continue from the frozen time - // In a more accurate implementation, you'd fetch the issue details to get exact pause duration setFrozenRemainingTime(null); } }, [lineDetail?.status, lineDetail?.productProcessIssueStatus]); + const handleSubmitOutput = async () => { if (!lineDetail?.id) return; - try { - // 直接使用 actions.ts 中定义的函数 await updateProductProcessLineQty({ productProcessLineId: lineDetail?.id || 0 as number, byproductName: outputData.byproductName, @@ -324,7 +343,6 @@ const ProductionProcessStepExecution: React.FC ({ ...prev, productProcessLineId: detail.id, - outputFromProcessQty: (detail as any).outputFromProcessQty || 0, // 取消注释,使用类型断言 - outputFromProcessUom: (detail as any).outputFromProcessUom || "", // 取消注释,使用类型断言 + outputFromProcessQty: (detail as any).outputFromProcessQty || 0, + outputFromProcessUom: (detail as any).outputFromProcessUom || "", defectQty: detail.defectQty || 0, defectUom: detail.defectUom || "", defectDescription: detail.defectDescription || "", @@ -382,7 +399,6 @@ const ProductionProcessStepExecution: React.FC { if (isManualScanning && qrValues.length > 0 && lineDetail?.id) { const latestQr = qrValues[qrValues.length - 1]; @@ -392,20 +408,88 @@ const ProductionProcessStepExecution: React.FC new Set(prev).add(latestQr)); - //processQrCode(latestQr); } }, [qrValues, isManualScanning, lineDetail?.id, processedQrCodes]); - - // 开始扫描 - - + const lineAssumeEndTime = useMemo(() => { + if (!lineDetail?.startTime || !lineDetail?.durationInMinutes) return null; + + // 解析 startTime(可能是数组或字符串) + let start: dayjs.Dayjs; + if (Array.isArray(lineDetail.startTime)) { + const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; + start = dayjs(new Date(year, month - 1, day, hour, minute, second)); + } else if (typeof lineDetail.startTime === 'string') { + // 检查是否是 "MM-DD HH:mm" 格式 + const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/; + const match = lineDetail.startTime.match(mmddHhmmPattern); + + if (match) { + const month = parseInt(match[1], 10); + const day = parseInt(match[2], 10); + const hour = parseInt(match[3], 10); + const minute = parseInt(match[4], 10); + + // 使用当前年份,但如果跨年(startTime 是年末,当前是年初),使用上一年 + const now = dayjs(); + let year = now.year(); + + if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) { + year = now.year() - 1; + } + + start = dayjs(new Date(year, month - 1, day, hour, minute, 0)); + } else { + start = dayjs(lineDetail.startTime); + } + } else { + start = dayjs(lineDetail.startTime as any); + } + + if (!start.isValid()) return null; + + return start.add(lineDetail.durationInMinutes, 'minute'); + }, [lineDetail?.startTime, lineDetail?.durationInMinutes]); + const lineStartTime = useMemo(() => { + if (!lineDetail?.startTime) return null; + + let start: dayjs.Dayjs; + if (Array.isArray(lineDetail.startTime)) { + const [year, month, day, hour = 0, minute = 0, second = 0] = lineDetail.startTime; + start = dayjs(new Date(year, month - 1, day, hour, minute, second)); + } else if (typeof lineDetail.startTime === 'string') { + const mmddHhmmPattern = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})$/; + const match = lineDetail.startTime.match(mmddHhmmPattern); + + if (match) { + const month = parseInt(match[1], 10); + const day = parseInt(match[2], 10); + const hour = parseInt(match[3], 10); + const minute = parseInt(match[4], 10); + + const now = dayjs(); + let year = now.year(); + + if (month === 12 && day >= 20 && now.month() === 0 && now.date() <= 10) { + year = now.year() - 1; + } + + start = dayjs(new Date(year, month - 1, day, hour, minute, 0)); + } else { + start = dayjs(lineDetail.startTime); + } + } else { + start = dayjs(lineDetail.startTime as any); + } + + return start.isValid() ? start : null; + }, [lineDetail?.startTime]); const handleOpenReasonModel = () => { setIsOpenReasonModel(true); - setPauseReason(""); // 重置原因 + setPauseReason(""); }; const handleCloseReasonModel = () => { setIsOpenReasonModel(false); - setPauseReason(""); // 清空原因 + setPauseReason(""); }; const handleSaveReason = async () => { if (!pauseReason.trim()) { @@ -421,7 +505,6 @@ const ProductionProcessStepExecution: React.FC { setLineDetail(detail as any); @@ -435,7 +518,6 @@ const ProductionProcessStepExecution: React.FC { if (!lineDetail?.productProcessIssueId) { console.error("No productProcessIssueId found"); @@ -446,13 +528,11 @@ const ProductionProcessStepExecution: React.FC { console.log("✅ Line detail refreshed after resume:", detail); setLineDetail(detail as any); - // Clear frozen time when resuming setFrozenRemainingTime(null); setLastPauseTime(null); }) @@ -465,6 +545,7 @@ const ProductionProcessStepExecution: React.FC @@ -472,18 +553,21 @@ const ProductionProcessStepExecution: React.FC - - {/* 如果已完成,显示合并的视图 */} + {processData && ( + + )} {isCompleted ? ( + {lineDetail?.status === "Pass" ? ( + + {t("Passed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) + + ) : ( - {t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) - - - {/**/} - - {/* 步骤信息部分 */} + {t("Completed Step")}: {lineDetail?.name} ({t("Seq")}: {lineDetail?.seqNo}) + + )} {t("Step Information")} @@ -510,9 +594,6 @@ const ProductionProcessStepExecution: React.FC - {/**/} - - {/* 产出数据部分 */} {t("Production Output Data")} @@ -526,7 +607,6 @@ const ProductionProcessStepExecution: React.FC - {/* Output from Process */} {t("Output from Process")} @@ -539,27 +619,6 @@ const ProductionProcessStepExecution: React.FC - {/* By-product */} - {/* - - - {t("By-product")} - {lineDetail.byproductName && ( - - ({lineDetail.byproductName}) - - )} - - - {lineDetail.byproductQty} - - - {lineDetail.byproductUom || "-"} - - - */} - {/* Defect */} - {t("Defect")} @@ -573,7 +632,6 @@ const ProductionProcessStepExecution: React.FC {lineDetail.defectDescription || "-"} - @@ -588,7 +646,6 @@ const ProductionProcessStepExecution: React.FC {lineDetail.defectDescription3 || "-"} - @@ -603,9 +660,7 @@ const ProductionProcessStepExecution: React.FC {lineDetail.defectDescription2 || "-"} - - {/* Scrap */} {t("Scrap")} @@ -623,8 +678,6 @@ const ProductionProcessStepExecution: React.FC ) : ( <> - {/* 如果未完成,显示原来的两个部分 */} - {/* 当前步骤信息 */} {!showOutputTable && ( @@ -654,6 +707,27 @@ const ProductionProcessStepExecution: React.FC {isOverTime ? `${t("Over Time")}: ${remainingTime}` : remainingTime} + {/* ✅ 添加:Process Start Time 和 Assume End Time */} + {/* ✅ 添加:Process Start Time 和 Assume End Time */} + {processData?.startTime && ( + + + {t("Process Start Time")}: {dayjs(processData.startTime).format("MM-DD")} {dayjs(processData.startTime).format("HH:mm")} + + + )} + {lineStartTime && ( + + + {t("Step Start Time")}: {lineStartTime.format("MM-DD")} {lineStartTime.format("HH:mm")} + + {lineAssumeEndTime && ( + + {t("Assume End Time")}: {lineAssumeEndTime.format("MM-DD")} {lineAssumeEndTime.format("HH:mm")} + + )} + + )} {lineDetail?.status === "Paused" && ( {t("Timer Paused")} @@ -662,17 +736,6 @@ const ProductionProcessStepExecution: React.FC )} - {/* - - */ - } { lineDetail?.status === 'InProgress'? ( @@ -710,8 +773,6 @@ const ProductionProcessStepExecution: React.FC - - @@ -723,7 +784,6 @@ const ProductionProcessStepExecution: React.FC - {/* start line output */} {t("Output from Process")} @@ -756,8 +816,6 @@ const ProductionProcessStepExecution: React.FC - - {/* defect 1 */} {t("Defect")}{t("(1)")} @@ -789,7 +847,6 @@ const ProductionProcessStepExecution: React.FC setOutputData({ ...outputData, defectDescription: e.target.value @@ -797,7 +854,6 @@ const ProductionProcessStepExecution: React.FC - {/* defect 2 */} {t("Defect")}{t("(2)")} @@ -829,7 +885,6 @@ const ProductionProcessStepExecution: React.FC setOutputData({ ...outputData, defectDescription2: e.target.value @@ -837,7 +892,6 @@ const ProductionProcessStepExecution: React.FC - {/* defect 3 */} {t("Defect")}{t("(3)")} @@ -869,7 +923,6 @@ const ProductionProcessStepExecution: React.FC setOutputData({ ...outputData, defectDescription3: e.target.value @@ -877,7 +930,6 @@ const ProductionProcessStepExecution: React.FC - {/* scrap */} {t("Scrap")} @@ -909,7 +961,6 @@ const ProductionProcessStepExecution: React.FC
- {/* submit button */}