From 4b9cd0f54de929c20f771299364ff22ce6044979 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 3 Dec 2025 11:06:38 +0800 Subject: [PATCH] update --- src/app/(main)/productionProcess/page.tsx | 48 + src/app/api/bom/index.ts | 1 + src/app/api/jo/actions.ts | 96 +- src/app/api/jo/index.ts | 2 + src/app/api/pickOrder/actions.ts | 4 + .../FinishedGoodFloorLanePanel.tsx | 4 +- src/components/JoSearch/JoCreateFormModal.tsx | 31 +- src/components/JoSearch/JoSearch.tsx | 30 +- src/components/JoSearch/JoSearchWrapper.tsx | 10 +- src/components/Jodetail/JoPickOrderDetail.tsx | 34 + src/components/Jodetail/JoPickOrderList.tsx | 176 ++ src/components/Jodetail/JobPickExecution.tsx | 4 +- src/components/Jodetail/JodetailSearch.tsx | 4 + .../Jodetail/newJobPickExecution.tsx | 1942 +++++++++++++++++ .../NavigationContent/NavigationContent.tsx | 7 + src/components/PickOrderSearch/LotTable.tsx | 41 +- .../ProcessSummaryHeader.tsx | 46 + .../ProductionProcessDetail.tsx | 206 +- .../ProductionProcessJobOrderDetail.tsx | 39 +- .../QrCodeScannerProvider.tsx | 52 +- src/i18n/zh/common.json | 9 +- src/i18n/zh/jo.json | 11 +- src/i18n/zh/pickOrder.json | 1 + 23 files changed, 2688 insertions(+), 110 deletions(-) create mode 100644 src/app/(main)/productionProcess/page.tsx create mode 100644 src/components/Jodetail/JoPickOrderDetail.tsx create mode 100644 src/components/Jodetail/JoPickOrderList.tsx create mode 100644 src/components/Jodetail/newJobPickExecution.tsx create mode 100644 src/components/ProductionProcess/ProcessSummaryHeader.tsx diff --git a/src/app/(main)/productionProcess/page.tsx b/src/app/(main)/productionProcess/page.tsx new file mode 100644 index 0000000..d3eea1f --- /dev/null +++ b/src/app/(main)/productionProcess/page.tsx @@ -0,0 +1,48 @@ +import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage"; +import { I18nProvider, getServerI18n } from "../../../i18n"; + +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; +import { fetchPrinterCombo } from "@/app/api/settings/printer"; + +export const metadata: Metadata = { + title: "Job Order Production Process", +}; + +const productionProcess: React.FC = async () => { + const { t } = await getServerI18n("common"); + const printerCombo = await fetchPrinterCombo(); + return ( + <> + + + {t("Job Order Production Process")} + + {/* Optional: Remove or modify create button, because creation is done via API automatically */} + {/* */} + + + + + + ); +}; + +export default productionProcess; \ No newline at end of file diff --git a/src/app/api/bom/index.ts b/src/app/api/bom/index.ts index d52be14..3e4ec62 100644 --- a/src/app/api/bom/index.ts +++ b/src/app/api/bom/index.ts @@ -6,6 +6,7 @@ export interface BomCombo { id: number; value: number; label: string; + outputQty: number; } export const preloadBomCombo = (() => { diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index dc0d844..2b92e31 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -26,6 +26,7 @@ export interface SearchJoResultRequest extends Pageable { itemName?: string; planStart?: string; planStartTo?: string; + jobTypeName?: string; } export interface productProcessLineQtyRequest { @@ -96,6 +97,8 @@ export interface JobOrderDetail { reqQty: number; uom: string; pickLines: any[]; + + jobTypeName: string; status: string; } @@ -183,6 +186,7 @@ export interface ProductProcessLineResponse { name: string, description: string, equipment_name: string, + equipmentDetailCode: string, status: string, byproductId: number, byproductName: string, @@ -215,6 +219,8 @@ export interface ProductProcessWithLinesResponse { isDark: string; isDense: number; isFloat: string; + scrapRate: number; + allergicSubstance: string; itemId: number; itemCode: string; itemName: string; @@ -301,8 +307,10 @@ export interface ProductProcessInfoResponse { } export interface ProductProcessLineQrscanUpadteRequest { productProcessLineId: number; - operatorId?: number; - equipmentId?: number; + //operatorId?: number; + //equipmentId?: number; + equipmentTypeSubTypeEquipmentNo?: string; + staffNo?: string; } export interface ProductProcessLineDetailResponse { @@ -403,6 +411,7 @@ export interface ProductProcessLineInfoResponse { name: string, description: string, equipment_name: string, + equipmentDetailCode: string, status: string, byproductId: number, byproductName: string, @@ -419,8 +428,74 @@ export interface ProductProcessLineInfoResponse { startTime: string, endTime: string } - - +export interface AllJoPickOrderResponse { + id: number; + pickOrderId: number | null; + pickOrderCode: string | null; + jobOrderId: number | null; + jobOrderCode: string | null; + jobOrderTypeId: number | null; + jobOrderType: string | null; + itemId: number; + itemName: string; + reqQty: number; + uomId: number; + uomName: string; + jobOrderStatus: string; + finishedPickOLineCount: number; +} +export interface UpdateJoPickOrderHandledByRequest { + pickOrderId: number; + itemId: number; + userId: number; +} +export interface JobTypeResponse { + id: number; + name: string; +} +export const deleteJobOrder=cache(async (jobOrderId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`, + { + method: "POST", + } + ); +}); +export const fetchAllJobTypes = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/jo/jobTypes`, + { + method: "GET", + } + ); +}); +export const updateJoPickOrderHandledBy = cache(async (request: UpdateJoPickOrderHandledByRequest) => { + return serverFetchJson( + `${BASE_API_URL}/jo/update-jo-pick-order-handled-by`, + { + method: "POST", + body: JSON.stringify(request), + headers: { "Content-Type": "application/json" }, + }, + ); +}); +export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`, + { + method: "GET", + next: { tags: ["jo-hierarchical"] }, + }, + ); +}); +export const fetchAllJoPickOrders = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/jo/AllJoPickOrder`, + { + method: "GET", + } + ); +}); export const fetchProductProcessLineDetail = cache(async (lineId: number) => { return serverFetchJson( `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, @@ -441,12 +516,23 @@ export const updateProductProcessLineQty = cache(async (request: UpdateProductPr }); export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => { + const requestBody: any = { + productProcessLineId: request.productProcessLineId, + //operatorId: request.operatorId, + //equipmentId: request.equipmentId, + equipmentTypeSubTypeEquipmentNo: request.equipmentTypeSubTypeEquipmentNo, + staffNo: request.staffNo, + }; + if (request.equipmentTypeSubTypeEquipmentNo !== undefined) { + requestBody["EquipmentType-SubType-EquipmentNo"] = request.equipmentTypeSubTypeEquipmentNo; + } + return serverFetchJson( `${BASE_API_URL}/product-process/Demo/update`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(request), + body: JSON.stringify(requestBody), } ); }); diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts index 239b6b7..43cd7bf 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -28,6 +28,8 @@ export interface JobOrder { planStartTo?: string; planEnd?: number[]; type: string; + jobTypeId: number; + jobTypeName: string; // TODO pack below into StockInLineInfo stockInLineId?: number; stockInLineStatus?: string; diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index b409d89..ce5fc0d 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -452,6 +452,10 @@ export interface LaneBtn { unassigned: number; total: number; } + + + + export const fetchDoPickOrderDetail = async ( doPickOrderId: number, selectedPickOrderId?: number diff --git a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx index 5207cd8..b655715 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material"; +import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSession } from "next-auth/react"; @@ -217,7 +217,7 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSw }} > {isLoadingSummary ? ( - Loading... + {t("Loading...")} ) : !summary2F?.rows || summary2F.rows.length === 0 ? ( = ({ const handleAutoCompleteChange = useCallback((event: SyntheticEvent, value: BomCombo, onChange: (...event: any[]) => void) => { onChange(value.id) + if (value.outputQty != null) { + formProps.setValue("reqQty", Number(value.outputQty), { shouldValidate: true, shouldDirty: true }) + } }, []) const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { @@ -156,16 +159,28 @@ const JoCreateFormModal: React.FC = ({ /> - value > 0 - })} - label={t("Req. Qty")} - fullWidth - error={Boolean(errors.reqQty)} - variant="outlined" - type="number" + }} + render={({ field, fieldState: { error } }) => ( + { + const val = e.target.value === "" ? undefined : Number(e.target.value); + field.onChange(val); + }} + /> + )} /> diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index be87dba..99d64bc 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -26,18 +26,19 @@ import dayjs from "dayjs"; import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { PrinterCombo } from "@/app/api/settings/printer"; - +import { JobTypeResponse } from "@/app/api/jo/actions"; interface Props { defaultInputs: SearchJoResultRequest, bomCombo: BomCombo[] printerCombo: PrinterCombo[]; + jobTypes: JobTypeResponse[]; } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo }) => { +const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => { const { t } = useTranslation("jo"); const router = useRouter() const [filteredJos, setFilteredJos] = useState([]); @@ -139,7 +140,16 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo }) => const searchCriteria: Criterion[] = useMemo(() => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Item Name"), paramName: "itemName", type: "text" }, - { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: dayjsToDateString(dayjs(), "input") }, + { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: { + from: dayjsToDateString(dayjs(), "input"), + to: dayjsToDateString(dayjs(), "input") + } }, + { + label: t("Job Type"), + paramName: "jobTypeName", + type: "select", + options: jobTypes.map(jt => jt.name) + }, ], [t]) const columns = useMemo[]>( @@ -205,6 +215,13 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo }) => ); } }, + { + name: "jobTypeName", + label: t("Job Type"), + renderCell: (row) => { + return row.jobTypeName ? t(row.jobTypeName) : '-' + } + }, { // TODO put it inside Action Buttons name: "id", @@ -271,6 +288,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo }) => planStartTo: query.planStartTo, pageNum: pagingController.pageNum - 1, pageSize: pagingController.pageSize, + jobTypeName: query.jobTypeName||"", } const response = await fetchJos(params) @@ -363,14 +381,16 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo }) => const transformedQuery = { ...query, planStart: query.planStart ? `${query.planStart}T00:00:00` : query.planStart, - planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo + planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo, + jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" }; setInputs(() => ({ code: transformedQuery.code, itemName: transformedQuery.itemName, planStart: transformedQuery.planStart, - planStartTo: transformedQuery.planStartTo + planStartTo: transformedQuery.planStartTo, + jobTypeName: transformedQuery.jobTypeName })) refetchData(transformedQuery, "search"); }, []) diff --git a/src/components/JoSearch/JoSearchWrapper.tsx b/src/components/JoSearch/JoSearchWrapper.tsx index 78470d6..256255b 100644 --- a/src/components/JoSearch/JoSearchWrapper.tsx +++ b/src/components/JoSearch/JoSearchWrapper.tsx @@ -4,7 +4,7 @@ import JoSearch from "./JoSearch"; import { SearchJoResultRequest } from "@/app/api/jo/actions"; import { fetchBomCombo } from "@/app/api/bom"; import { fetchPrinterCombo } from "@/app/api/settings/printer"; - +import { fetchAllJobTypes } from "@/app/api/jo/actions"; interface SubComponents { Loading: typeof GeneralLoading; } @@ -17,13 +17,15 @@ const JoSearchWrapper: React.FC & SubComponents = async () => { const [ bomCombo, - printerCombo + printerCombo, + jobTypes ] = await Promise.all([ fetchBomCombo(), - fetchPrinterCombo() + fetchPrinterCombo(), + fetchAllJobTypes() ]) - return + return } JoSearchWrapper.Loading = GeneralLoading; diff --git a/src/components/Jodetail/JoPickOrderDetail.tsx b/src/components/Jodetail/JoPickOrderDetail.tsx new file mode 100644 index 0000000..9008c2f --- /dev/null +++ b/src/components/Jodetail/JoPickOrderDetail.tsx @@ -0,0 +1,34 @@ +"use client"; +import React, { useCallback } from "react"; +import { Box, Button, Stack } from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import { useTranslation } from "react-i18next"; +import JobPickExecution from "./JobPickExecution"; + +interface JoPickOrderDetailProps { + pickOrderId: number | undefined; + jobOrderId: number | undefined; + onBack: () => void; +} + +const JoPickOrderDetail: React.FC = ({ + pickOrderId, + jobOrderId, + onBack, +}) => { + const { t } = useTranslation("jo"); + + return ( + + + + + + + + ); +}; + +export default JoPickOrderDetail; \ No newline at end of file diff --git a/src/components/Jodetail/JoPickOrderList.tsx b/src/components/Jodetail/JoPickOrderList.tsx new file mode 100644 index 0000000..ac6968a --- /dev/null +++ b/src/components/Jodetail/JoPickOrderList.tsx @@ -0,0 +1,176 @@ +"use client"; +import React, { useCallback, useEffect, useState } from "react"; +import { + Box, + Button, + Card, + CardContent, + CardActions, + Stack, + Typography, + Chip, + CircularProgress, + TablePagination, + Grid, +} from "@mui/material"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import { useTranslation } from "react-i18next"; +import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions"; +import JobPickExecution from "./newJobPickExecution"; + +const PER_PAGE = 6; + +const JoPickOrderList: React.FC = () => { + const { t } = useTranslation(["common", "jo"]); + const [loading, setLoading] = useState(false); + const [pickOrders, setPickOrders] = useState([]); + const [page, setPage] = useState(0); + const [selectedPickOrderId, setSelectedPickOrderId] = useState(undefined); + const [selectedJobOrderId, setSelectedJobOrderId] = useState(undefined); + + const fetchPickOrders = useCallback(async () => { + setLoading(true); + try { + const data = await fetchAllJoPickOrders(); + setPickOrders(Array.isArray(data) ? data : []); + setPage(0); + } catch (e) { + console.error(e); + setPickOrders([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchPickOrders(); + }, [fetchPickOrders]); + + // If a pick order is selected, show JobPickExecution detail view + if (selectedPickOrderId !== undefined) { + return ( + + + + + + + ); + } + + const startIdx = page * PER_PAGE; + const paged = pickOrders.slice(startIdx, startIdx + PER_PAGE); + + return ( + + {loading ? ( + + + + ) : ( + + + {t("Total pick orders")}: {pickOrders.length} + + + + {paged.map((pickOrder) => { + const status = String(pickOrder.jobOrderStatus || ""); + const statusLower = status.toLowerCase(); + const statusColor = + statusLower === "completed" + ? "success" + : statusLower === "pending" || statusLower === "processing" + ? "primary" + : "default"; + + const finishedCount = pickOrder.finishedPickOLineCount ?? 0; + + return ( + + + + + + + {t("Job Order")}: {pickOrder.jobOrderCode || "-"} + + + + + + + {t("Pick Order")}: {pickOrder.pickOrderCode || "-"} + + + {t("Item Name")}: {pickOrder.itemName} + + + {t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName}) + + {statusLower !== "pending" && finishedCount > 0 && ( + + + {t("Finished lines")}: {finishedCount} + + + )} + + + + + + + + + ); + })} + + {pickOrders.length > 0 && ( + setPage(p)} + rowsPerPageOptions={[PER_PAGE]} + /> + )} + + )} + + ); +}; + +export default JoPickOrderList; \ No newline at end of file diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index 4b87606..970a31c 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -457,7 +457,9 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); // TODO: Implement QR code functionality }; - + const getPickOrderId = useCallback(() => { + return filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + }, [filterArgs?.pickOrderId]); // 修改:使用 Job Order API 获取数据 const fetchJobOrderData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index a75977a..0d7b1b4 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -26,6 +26,7 @@ import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan"; import FInishedJobOrderRecord from "./FInishedJobOrderRecord"; import JobPickExecution from "./JobPickExecution"; import CompleteJobOrderRecord from "./completeJobOrderRecord"; +import JoPickOrderList from "./JoPickOrderList"; import { fetchUnassignedJobOrderPickOrders, assignJobOrderPickOrder, @@ -35,6 +36,7 @@ import { } from "@/app/api/jo/actions"; import { fetchPrinterCombo } from "@/app/api/settings/printer"; import { PrinterCombo } from "@/app/api/settings/printer"; +import JoPickOrderDetail from "./JoPickOrderDetail"; interface Props { pickOrders: PickOrderResult[]; printerCombo: PrinterCombo[]; @@ -474,6 +476,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; + {/* */} {/* */} {/* */} @@ -486,6 +489,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; }}> {tabIndex === 0 && } {tabIndex === 1 && } + {/* {tabIndex === 2 && } */} {/* {tabIndex === 2 && } */} {/* {tabIndex === 3 && } */} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx new file mode 100644 index 0000000..5a89614 --- /dev/null +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -0,0 +1,1942 @@ +"use client"; + +import { + Box, + Button, + Stack, + TextField, + Typography, + Alert, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TablePagination, + Modal, +} from "@mui/material"; +import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; +import { useCallback, useEffect, useState, useRef, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; +import { + updateStockOutLineStatus, + createStockOutLine, + recordPickExecutionIssue, + fetchFGPickOrders, + FGPickOrderResponse, + autoAssignAndReleasePickOrder, + AutoAssignReleaseResponse, + checkPickOrderCompletion, + PickOrderCompletionResponse, + checkAndCompletePickOrderByConsoCode, + confirmLotSubstitution +} from "@/app/api/pickOrder/actions"; +// 修改:使用 Job Order API +import { + //fetchJobOrderLotsHierarchical, + //fetchUnassignedJobOrderPickOrders, + assignJobOrderPickOrder, + fetchJobOrderLotsHierarchicalByPickOrderId, + updateJoPickOrderHandledBy +} from "@/app/api/jo/actions"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { + FormProvider, + useForm, +} from "react-hook-form"; +import SearchBox, { Criterion } from "../SearchBox"; +import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; +import { updateInventoryLotLineQuantities, analyzeQrCode, fetchLotDetail } from "@/app/api/inventory/actions"; +import QrCodeIcon from '@mui/icons-material/QrCode'; +import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import { fetchStockInLineInfo } from "@/app/api/po/actions"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; +import FGPickOrderCard from "./FGPickOrderCard"; +import LotConfirmationModal from "./LotConfirmationModal"; +interface Props { + filterArgs: Record; +} + +// QR Code Modal Component (from GoodPickExecution) +const QrCodeModal: React.FC<{ + open: boolean; + onClose: () => void; + lot: any | null; + onQrCodeSubmit: (lotNo: string) => void; + combinedLotData: any[]; +}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { + const { t } = useTranslation("jo"); + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + const [manualInput, setManualInput] = useState(''); + + const [manualInputSubmitted, setManualInputSubmitted] = useState(false); + const [manualInputError, setManualInputError] = useState(false); + const [isProcessingQr, setIsProcessingQr] = useState(false); + const [qrScanFailed, setQrScanFailed] = useState(false); + const [qrScanSuccess, setQrScanSuccess] = useState(false); + + const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); + const [scannedQrResult, setScannedQrResult] = useState(''); + + // Process scanned QR codes + useEffect(() => { + if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr)) { + console.log("QR code already processed, skipping..."); + return; + } + + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + + try { + const qrData = JSON.parse(latestQr); + + if (qrData.stockInLineId && qrData.itemId) { + setIsProcessingQr(true); + setQrScanFailed(false); + + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Stock in line info:", stockInLineInfo); + setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); + + if (stockInLineInfo.lotNo === lot.lotNo) { + console.log(` QR Code verified for lot: ${lot.lotNo}`); + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`); + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + }) + .catch((error) => { + console.error("Error fetching stock in line info:", error); + setScannedQrResult('Error fetching data'); + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + }) + .finally(() => { + setIsProcessingQr(false); + }); + } else { + const qrContent = latestQr.replace(/[{}]/g, ''); + setScannedQrResult(qrContent); + + if (qrContent === lot.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + } + } catch (error) { + console.log("QR code is not JSON format, trying direct comparison"); + const qrContent = latestQr.replace(/[{}]/g, ''); + setScannedQrResult(qrContent); + + if (qrContent === lot.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + } + } + }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]); + + // Clear states when modal opens + useEffect(() => { + if (open) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + setIsProcessingQr(false); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); + } + }, [open]); + + useEffect(() => { + if (lot) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + setIsProcessingQr(false); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); + } + }, [lot]); + + // Auto-submit manual input when it matches + useEffect(() => { + if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) { + console.log(' Auto-submitting manual input:', manualInput.trim()); + + const timer = setTimeout(() => { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + setManualInputError(false); + setManualInputSubmitted(false); + }, 200); + + return () => clearTimeout(timer); + } + }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]); + + const handleManualSubmit = () => { + if (manualInput.trim() === lot?.lotNo) { + setQrScanSuccess(true); + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + } else { + setQrScanFailed(true); + setManualInputError(true); + setManualInputSubmitted(true); + } + }; + + useEffect(() => { + if (open) { + startScan(); + } + }, [open, startScan]); + + return ( + + + + {t("QR Code Scan for Lot")}: {lot?.lotNo} + + + {isProcessingQr && ( + + + {t("Processing QR code...")} + + + )} + + + + {t("Manual Input")}: + + { + setManualInput(e.target.value); + if (qrScanFailed || manualInputError) { + setQrScanFailed(false); + setManualInputError(false); + setManualInputSubmitted(false); + } + }} + sx={{ mb: 1 }} + error={manualInputSubmitted && manualInputError} + helperText={ + manualInputSubmitted && manualInputError + ? `${t("The input is not the same as the expected lot number.")}` + : '' + } + /> + + + + {qrValues.length > 0 && ( + + + {t("QR Scan Result:")} {scannedQrResult} + + + {qrScanSuccess && ( + + {t("Verified successfully!")} + + )} + + )} + + + + + + + ); +}; + +const JobPickExecution: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("jo"); + const router = useRouter(); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + + const currentUserId = session?.id ? parseInt(session.id) : undefined; + + // 修改:使用 Job Order 数据结构 + const [jobOrderData, setJobOrderData] = useState(null); + const [combinedLotData, setCombinedLotData] = useState([]); + const [combinedDataLoading, setCombinedDataLoading] = useState(false); + const [originalCombinedData, setOriginalCombinedData] = useState([]); + + // 添加未分配订单状态 + const [unassignedOrders, setUnassignedOrders] = useState([]); + const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); + + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); + const [expectedLotData, setExpectedLotData] = useState(null); + const [scannedLotData, setScannedLotData] = useState(null); + const [isConfirmingLot, setIsConfirmingLot] = useState(false); + const [qrScanInput, setQrScanInput] = useState(''); + const [qrScanError, setQrScanError] = useState(false); + const [qrScanSuccess, setQrScanSuccess] = useState(false); + + const [pickQtyData, setPickQtyData] = useState>({}); + const [searchQuery, setSearchQuery] = useState>({}); + + const [paginationController, setPaginationController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + const [usernameList, setUsernameList] = useState([]); + + const initializationRef = useRef(false); + const autoAssignRef = useRef(false); + + const formProps = useForm(); + const errors = formProps.formState.errors; + const [isSubmittingAll, setIsSubmittingAll] = useState(false); + + // Add QR modal states + const [qrModalOpen, setQrModalOpen] = useState(false); + const [selectedLotForQr, setSelectedLotForQr] = useState(null); + + // Add GoodPickExecutionForm states + const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); + const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null); + const [fgPickOrders, setFgPickOrders] = useState([]); + const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); + + // Add these missing state variables + const [isManualScanning, setIsManualScanning] = useState(false); + const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); + const [lastProcessedQr, setLastProcessedQr] = useState(''); + const [isRefreshingData, setIsRefreshingData] = useState(false); + + // 修改:加载未分配的 Job Order 订单 + const loadUnassignedOrders = useCallback(async () => { + setIsLoadingUnassigned(true); + try { + //const orders = await fetchUnassignedJobOrderPickOrders(); + //setUnassignedOrders(orders); + } catch (error) { + console.error("Error loading unassigned orders:", error); + } finally { + setIsLoadingUnassigned(false); + } + }, []); + + // 修改:分配订单给当前用户 + const handleAssignOrder = useCallback(async (pickOrderId: number) => { + if (!currentUserId) { + console.error("Missing user id in session"); + return; + } + + try { + const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); + if (result.message === "Successfully assigned") { + console.log(" Successfully assigned pick order"); + // 刷新数据 + window.dispatchEvent(new CustomEvent('pickOrderAssigned')); + // 重新加载未分配订单列表 + loadUnassignedOrders(); + } else { + console.warn("⚠️ Assignment failed:", result.message); + alert(`Assignment failed: ${result.message}`); + } + } catch (error) { + console.error("❌ Error assigning order:", error); + alert("Error occurred during assignment"); + } + }, [currentUserId, loadUnassignedOrders]); + + const fetchFgPickOrdersData = useCallback(async () => { + if (!currentUserId) return; + + setFgPickOrdersLoading(true); + try { + // Get all pick order IDs from combinedLotData + const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId))); + + if (pickOrderIds.length === 0) { + setFgPickOrders([]); + return; + } + + // Fetch FG pick orders for each pick order ID + const fgPickOrdersPromises = pickOrderIds.map(pickOrderId => + fetchFGPickOrders(pickOrderId) + ); + + const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises); + + // Flatten the results (each fetchFGPickOrders returns an array) + const allFgPickOrders = fgPickOrdersResults.flat(); + + setFgPickOrders(allFgPickOrders); + console.log(" Fetched FG pick orders:", allFgPickOrders); + } catch (error) { + console.error("❌ Error fetching FG pick orders:", error); + setFgPickOrders([]); + } finally { + setFgPickOrdersLoading(false); + } + }, [currentUserId, combinedLotData]); + + useEffect(() => { + if (combinedLotData.length > 0) { + fetchFgPickOrdersData(); + } + }, [combinedLotData, fetchFgPickOrdersData]); + + // Handle QR code button click + const handleQrCodeClick = (pickOrderId: number) => { + console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); + // TODO: Implement QR code functionality + }; + + // 修改:使用 Job Order API 获取数据 + const fetchJobOrderData = useCallback(async (pickOrderId?: number) => { + setCombinedDataLoading(true); + try { + if (!pickOrderId) { + console.warn("⚠️ No pickOrderId provided, skipping API call"); + setJobOrderData(null); + setCombinedLotData([]); + setOriginalCombinedData([]); + return; + } + + console.log("🔍 Fetching job order data by pickOrderId:", pickOrderId); + + window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { + detail: { + hasData: false, + tabIndex: 0 + } + })); + + const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId); + console.log("✅ Job Order data:", jobOrderData); + + setJobOrderData(jobOrderData); + + // Transform hierarchical data to flat structure for the table + const flatLotData: any[] = []; + + if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { + jobOrderData.pickOrderLines.forEach((line: any) => { + if (line.lots && line.lots.length > 0) { + line.lots.forEach((lot: any) => { + flatLotData.push({ + pickOrderId: jobOrderData.pickOrder.id, + pickOrderCode: jobOrderData.pickOrder.code, + pickOrderConsoCode: jobOrderData.pickOrder.consoCode, + pickOrderTargetDate: jobOrderData.pickOrder.targetDate, + pickOrderType: jobOrderData.pickOrder.type, + pickOrderStatus: jobOrderData.pickOrder.status, + pickOrderAssignTo: jobOrderData.pickOrder.assignTo, + + // Pick order line info + pickOrderLineId: line.id, + pickOrderLineRequiredQty: line.requiredQty, + pickOrderLineStatus: line.status, + + // Item info + itemId: line.itemId, + itemCode: line.itemCode, + itemName: line.itemName, + uomCode: line.uomCode, + uomDesc: line.uomDesc, + + // Lot info + lotId: lot.lotId, + lotNo: lot.lotNo, + expiryDate: lot.expiryDate, + location: lot.location, + availableQty: lot.availableQty, + requiredQty: lot.requiredQty, + actualPickQty: lot.actualPickQty, + lotStatus: lot.lotStatus, + lotAvailability: lot.lotAvailability, + processingStatus: lot.processingStatus, + stockOutLineId: lot.stockOutLineId, + stockOutLineStatus: lot.stockOutLineStatus, + stockOutLineQty: lot.stockOutLineQty, + suggestedPickLotId: lot.suggestedPickLotId, + // Router info + routerIndex: lot.routerIndex, + secondQrScanStatus: lot.secondQrScanStatus, + routerArea: lot.routerArea, + routerRoute: lot.routerRoute, + uomShortDesc: lot.uomShortDesc + }); + }); + } + }); + } + + console.log("✅ Transformed flat lot data:", flatLotData); + setCombinedLotData(flatLotData); + setOriginalCombinedData(flatLotData); + + const hasData = flatLotData.length > 0; + window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { + detail: { + hasData: hasData, + tabIndex: 0 + } + })); + + // Calculate completion status and send event + const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => + lot.processingStatus === 'completed' + ); + + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: allCompleted, + tabIndex: 0 + } + })); + + } catch (error) { + console.error("❌ Error fetching job order data:", error); + setJobOrderData(null); + setCombinedLotData([]); + setOriginalCombinedData([]); + + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: false, + tabIndex: 0 + } + })); + } finally { + setCombinedDataLoading(false); + } + }, []); + const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { + if (!currentUserId || !pickOrderId || !itemId) { + return; + } + + try { + console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`); + await updateJoPickOrderHandledBy({ + pickOrderId: pickOrderId, + itemId: itemId, + userId: currentUserId + }); + console.log("✅ JoPickOrder.handledBy updated successfully"); + } catch (error) { + console.error("❌ Error updating JoPickOrder.handledBy:", error); + // Don't throw - this is not critical for the main flow + } + }, [currentUserId]); + // 修改:初始化时加载数据 + useEffect(() => { + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing job order..."); + initializationRef.current = true; + + // Get pickOrderId from filterArgs if available (when viewing from list) + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + if (pickOrderId) { + fetchJobOrderData(pickOrderId); + } + loadUnassignedOrders(); + } + }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]); + + // Add event listener for manual assignment + useEffect(() => { + const handlePickOrderAssigned = () => { + console.log("🔄 Pick order assigned event received, refreshing data..."); + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + if (pickOrderId) { + fetchJobOrderData(pickOrderId); + } + }; + + window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); + + return () => { + window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); + }; + }, [fetchJobOrderData, filterArgs?.pickOrderId]); + + // Handle QR code submission for matched lot (external scanning) + const handleQrCodeSubmit = useCallback(async (lotNo: string) => { + console.log(` Processing QR Code for lot: ${lotNo}`); + + // Use current data without refreshing to avoid infinite loop + const currentLotData = combinedLotData; + console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo)); + + const matchingLots = currentLotData.filter(lot => + lot.lotNo === lotNo || + lot.lotNo?.toLowerCase() === lotNo.toLowerCase() + ); + + if (matchingLots.length === 0) { + console.error(`❌ Lot not found: ${lotNo}`); + setQrScanError(true); + setQrScanSuccess(false); + const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', '); + console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`); + return; + } + + console.log(` Found ${matchingLots.length} matching lots:`, matchingLots); + setQrScanError(false); + + try { + let successCount = 0; + let errorCount = 0; + + for (const matchingLot of matchingLots) { + console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); + + if (matchingLot.stockOutLineId) { + const stockOutLineUpdate = await updateStockOutLineStatus({ + id: matchingLot.stockOutLineId, + status: 'checked', + qty: 0 + }); + console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); + + // Treat multiple backend shapes as success (type-safe via any) + const r: any = stockOutLineUpdate as any; + const updateOk = + r?.code === 'SUCCESS' || + typeof r?.id === 'number' || + r?.type === 'checked' || + r?.status === 'checked' || + typeof r?.entity?.id === 'number' || + r?.entity?.status === 'checked'; + + if (updateOk) { + successCount++; + } else { + errorCount++; + } + } else { + const createStockOutLineData = { + consoCode: matchingLot.pickOrderConsoCode, + pickOrderLineId: matchingLot.pickOrderLineId, + inventoryLotLineId: matchingLot.lotId, + qty: 0 + }; + + const createResult = await createStockOutLine(createStockOutLineData); + console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult); + + if (createResult && createResult.code === "SUCCESS") { + // Immediately set status to checked for new line + let newSolId: number | undefined; + const anyRes: any = createResult as any; + if (typeof anyRes?.id === 'number') { + newSolId = anyRes.id; + } else if (anyRes?.entity) { + newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id; + } + + if (newSolId) { + const setChecked = await updateStockOutLineStatus({ + id: newSolId, + status: 'checked', + qty: 0 + }); + if (setChecked && setChecked.code === "SUCCESS") { + successCount++; + } else { + errorCount++; + } + } else { + console.warn("Created stock out line but no ID returned; cannot set to checked"); + errorCount++; + } + } else { + errorCount++; + } + } + } + + // FIXED: Set refresh flag before refreshing data + setIsRefreshingData(true); + console.log("🔄 Refreshing data after QR code processing..."); + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + + if (successCount > 0) { + console.log(` QR Code processing completed: ${successCount} updated/created`); + setQrScanSuccess(true); + setQrScanError(false); + setQrScanInput(''); // Clear input after successful processing + + } else { + console.error(`❌ QR Code processing failed: ${errorCount} errors`); + setQrScanError(true); + setQrScanSuccess(false); + } + } catch (error) { + console.error("❌ Error processing QR code:", error); + setQrScanError(true); + setQrScanSuccess(false); + + // Still refresh data even on error + setIsRefreshingData(true); + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData( pickOrderId); + } finally { + // Clear refresh flag after a short delay + setTimeout(() => { + setIsRefreshingData(false); + }, 1000); + } + }, [combinedLotData, fetchJobOrderData]); + const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { + console.log("Lot mismatch detected:", { expectedLot, scannedLot }); + setExpectedLotData(expectedLot); + setScannedLotData(scannedLot); + setLotConfirmationOpen(true); + }, []); + + // Add handleLotConfirmation function + const handleLotConfirmation = useCallback(async () => { + if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; + setIsConfirmingLot(true); + try { + let newLotLineId = scannedLotData?.inventoryLotLineId; + if (!newLotLineId && scannedLotData?.stockInLineId) { + const ld = await fetchLotDetail(scannedLotData.stockInLineId); + newLotLineId = ld.inventoryLotLineId; + } + if (!newLotLineId) { + console.error("No inventory lot line id for scanned lot"); + return; + } + + console.log("=== Lot Confirmation Debug ==="); + console.log("Selected Lot:", selectedLotForQr); + console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId); + console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId); + console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId); + console.log("Lot ID (fallback):", selectedLotForQr.lotId); + console.log("New Inventory Lot Line ID:", newLotLineId); + + // Call confirmLotSubstitution to update the suggested lot + const substitutionResult = await confirmLotSubstitution({ + pickOrderLineId: selectedLotForQr.pickOrderLineId, + stockOutLineId: selectedLotForQr.stockOutLineId, + originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, + newInventoryLotLineId: newLotLineId + }); + + console.log(" Lot substitution result:", substitutionResult); + + // Update stock out line status to 'checked' after substitution + if(selectedLotForQr?.stockOutLineId){ + await updateStockOutLineStatus({ + id: selectedLotForQr.stockOutLineId, + status: 'checked', + qty: 0 + }); + console.log(" Stock out line status updated to 'checked'"); + } + + // Close modal and clean up state BEFORE refreshing + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + + // Clear QR processing state but DON'T clear processedQrCodes yet + setQrScanError(false); + setQrScanSuccess(true); + setQrScanInput(''); + + // Set refreshing flag to prevent QR processing during refresh + setIsRefreshingData(true); + + // Refresh data to show updated lot + console.log("🔄 Refreshing job order data..."); + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + console.log(" Lot substitution confirmed and data refreshed"); + + // Clear processed QR codes and flags immediately after refresh + // This allows new QR codes to be processed right away + setTimeout(() => { + console.log(" Clearing processed QR codes and resuming scan"); + setProcessedQrCodes(new Set()); + setLastProcessedQr(''); + setQrScanSuccess(false); + setIsRefreshingData(false); + }, 500); // Reduced from 3000ms to 500ms - just enough for UI update + + } catch (error) { + console.error("Error confirming lot substitution:", error); + setQrScanError(true); + setQrScanSuccess(false); + // Clear refresh flag on error + setIsRefreshingData(false); + } finally { + setIsConfirmingLot(false); + } + }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); + + const processOutsideQrCode = useCallback(async (latestQr: string) => { + // Don't process if confirmation modal is open + if (lotConfirmationOpen) { + console.log("⏸️ Confirmation modal is open, skipping QR processing"); + return; + } + + let qrData: any = null; + try { + qrData = JSON.parse(latestQr); + } catch { + console.log("QR is not JSON format"); + // Handle non-JSON QR codes as direct lot numbers + const directLotNo = latestQr.replace(/[{}]/g, ''); + if (directLotNo) { + console.log(`Processing direct lot number: ${directLotNo}`); + await handleQrCodeSubmit(directLotNo); + } + return; + } + + try { + // Only use the new API when we have JSON with stockInLineId + itemId + if (!(qrData?.stockInLineId && qrData?.itemId)) { + console.log("QR JSON missing required fields (itemId, stockInLineId)."); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // First, fetch stock in line info to get the lot number + let stockInLineInfo: any; + try { + stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId); + console.log("Stock in line info:", stockInLineInfo); + } catch (error) { + console.error("Error fetching stock in line info:", error); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // Call new analyze-qr-code API + const analysis = await analyzeQrCode({ + itemId: qrData.itemId, + stockInLineId: qrData.stockInLineId + }); + + if (!analysis) { + console.error("analyzeQrCode returned no data"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + const { + itemId: analyzedItemId, + itemCode: analyzedItemCode, + itemName: analyzedItemName, + scanned, + } = analysis || {}; + + // 1) Find all lots for the same item from current expected list + const sameItemLotsInExpected = combinedLotData.filter(l => + (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || + (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) + ); + + if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { + // Case 3: No item code match + console.error("No item match in expected lots for scanned code"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // Find the ACTIVE suggested lot (not rejected lots) + const activeSuggestedLots = sameItemLotsInExpected.filter(lot => + lot.lotAvailability !== 'rejected' && + lot.stockOutLineStatus !== 'rejected' && + lot.stockOutLineStatus !== 'completed' + ); + + if (activeSuggestedLots.length === 0) { + console.warn("All lots for this item are rejected or completed"); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + + // Use the first active suggested lot as the "expected" lot + const expectedLot = activeSuggestedLots[0]; + + // 2) Check if the scanned lot matches exactly + if (scanned?.lotNo === expectedLot.lotNo) { + // Case 1: Exact match - process normally + console.log(` Exact lot match: ${scanned.lotNo}`); + await handleQrCodeSubmit(scanned.lotNo); + return; + } + + // Case 2: Same item, different lot - show confirmation modal + console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); + + // DON'T stop scanning - just pause QR processing by showing modal + setSelectedLotForQr(expectedLot); + handleLotMismatch( + { + lotNo: expectedLot.lotNo, + itemCode: analyzedItemCode || expectedLot.itemCode, + itemName: analyzedItemName || expectedLot.itemName + }, + { + lotNo: scanned?.lotNo || '', + itemCode: analyzedItemCode || expectedLot.itemCode, + itemName: analyzedItemName || expectedLot.itemName, + inventoryLotLineId: scanned?.inventoryLotLineId, + stockInLineId: qrData.stockInLineId + } + ); + } catch (error) { + console.error("Error during analyzeQrCode flow:", error); + setQrScanError(true); + setQrScanSuccess(false); + return; + } + }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); + + + const handleManualInputSubmit = useCallback(() => { + if (qrScanInput.trim() !== '') { + handleQrCodeSubmit(qrScanInput.trim()); + } + }, [qrScanInput, handleQrCodeSubmit]); + + // Handle QR code submission from modal (internal scanning) + const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => { + if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { + console.log(` QR Code verified for lot: ${lotNo}`); + + const requiredQty = selectedLotForQr.requiredQty; + const lotId = selectedLotForQr.lotId; + + // Create stock out line + const stockOutLineData: CreateStockOutLine = { + consoCode: selectedLotForQr.pickOrderConsoCode, + pickOrderLineId: selectedLotForQr.pickOrderLineId, + inventoryLotLineId: selectedLotForQr.lotId, + qty: 0.0 + }; + + try { + await createStockOutLine(stockOutLineData); + console.log("Stock out line created successfully!"); + + // Close modal + setQrModalOpen(false); + setSelectedLotForQr(null); + + // Set pick quantity + const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`; + setTimeout(() => { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: requiredQty + })); + console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); + }, 500); + + // Refresh data + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + } catch (error) { + console.error("Error creating stock out line:", error); + } + } + }, [selectedLotForQr, fetchJobOrderData]); + + + useEffect(() => { + // Add isManualScanning check + if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) { + return; + } + + const latestQr = qrValues[qrValues.length - 1]; + + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("QR code already processed, skipping..."); + return; + } + + if (latestQr && latestQr !== lastProcessedQr) { + console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); + setLastProcessedQr(latestQr); + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + + processOutsideQrCode(latestQr); + } + }, [qrValues, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData, isManualScanning, lotConfirmationOpen]); + + const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { + if (value === '' || value === null || value === undefined) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + const numericValue = typeof value === 'string' ? parseFloat(value) : value; + + if (isNaN(numericValue)) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + setPickQtyData(prev => ({ + ...prev, + [lotKey]: numericValue + })); + }, []); + + const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle'); + const [autoAssignMessage, setAutoAssignMessage] = useState(''); + const [completionStatus, setCompletionStatus] = useState(null); + + const checkAndAutoAssignNext = useCallback(async () => { + if (!currentUserId) return; + + try { + const completionResponse = await checkPickOrderCompletion(currentUserId); + + if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { + console.log("Found completed pick orders, auto-assigning next..."); + // 移除前端的自动分配逻辑,因为后端已经处理了 + // await handleAutoAssignAndRelease(); // 删除这个函数 + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + }, [currentUserId]); + + // Handle submit pick quantity + const handleSubmitPickQty = useCallback(async (lot: any) => { + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + const newQty = pickQtyData[lotKey] || 0; + + if (!lot.stockOutLineId) { + console.error("No stock out line found for this lot"); + return; + } + + try { + const currentActualPickQty = lot.actualPickQty || 0; + const cumulativeQty = currentActualPickQty + newQty; + + let newStatus = 'partially_completed'; + + if (cumulativeQty >= lot.requiredQty) { + newStatus = 'completed'; + } + + console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Required Qty: ${lot.requiredQty}`); + console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); + console.log(`New Submitted Qty: ${newQty}`); + console.log(`Cumulative Qty: ${cumulativeQty}`); + console.log(`New Status: ${newStatus}`); + console.log(`=====================================`); + + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: newStatus, + qty: cumulativeQty + }); + + if (newQty > 0) { + await updateInventoryLotLineQuantities({ + inventoryLotLineId: lot.lotId, + qty: newQty, + status: 'available', + operation: 'pick' + }); + } + + // FIXED: Use the proper API function instead of direct fetch + if (newStatus === 'completed' && lot.pickOrderConsoCode) { + console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); + + try { + // Use the imported API function instead of direct fetch + const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); + console.log(` Pick order completion check result:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion: ${completionResponse.message}`); + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + } + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + console.log("Pick quantity submitted successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + } catch (error) { + console.error("Error submitting pick quantity:", error); + } + }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]); + const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { + if (!lot.stockOutLineId) { + console.error("No stock out line found for this lot"); + return; + } + + try { + // FIXED: Calculate cumulative quantity correctly + const currentActualPickQty = lot.actualPickQty || 0; + const cumulativeQty = currentActualPickQty + submitQty; + + // FIXED: Determine status based on cumulative quantity vs required quantity + let newStatus = 'partially_completed'; + + if (cumulativeQty >= lot.requiredQty) { + newStatus = 'completed'; + } else if (cumulativeQty > 0) { + newStatus = 'partially_completed'; + } else { + newStatus = 'checked'; // QR scanned but no quantity submitted yet + } + + console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Required Qty: ${lot.requiredQty}`); + console.log(`Current Actual Pick Qty: ${currentActualPickQty}`); + console.log(`New Submitted Qty: ${submitQty}`); + console.log(`Cumulative Qty: ${cumulativeQty}`); + console.log(`New Status: ${newStatus}`); + console.log(`=====================================`); + + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: newStatus, + qty: cumulativeQty // Use cumulative quantity + }); + + if (submitQty > 0) { + await updateInventoryLotLineQuantities({ + inventoryLotLineId: lot.lotId, + qty: submitQty, + status: 'available', + operation: 'pick' + }); + } + + // Check if pick order is completed when lot status becomes 'completed' + if (newStatus === 'completed' && lot.pickOrderConsoCode) { + console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); + + try { + const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); + console.log(` Pick order completion check result:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion: ${completionResponse.message}`); + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + } + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + console.log("Pick quantity submitted successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + } catch (error) { + console.error("Error submitting pick quantity:", error); + } + }, [fetchJobOrderData, checkAndAutoAssignNext]); + const handleSubmitAllScanned = useCallback(async () => { + const scannedLots = combinedLotData.filter(lot => + lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted + ); + + if (scannedLots.length === 0) { + console.log("No scanned items to submit"); + return; + } + + setIsSubmittingAll(true); + console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); + + try { + // Submit all items in parallel using Promise.all + const submitPromises = scannedLots.map(async (lot) => { + const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; + const currentActualPickQty = lot.actualPickQty || 0; + const cumulativeQty = currentActualPickQty + submitQty; + + let newStatus = 'partially_completed'; + if (cumulativeQty >= lot.requiredQty) { + newStatus = 'completed'; + } + + console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`); + + // Update stock out line + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: newStatus, + qty: cumulativeQty + }); + + // Update inventory + if (submitQty > 0) { + await updateInventoryLotLineQuantities({ + inventoryLotLineId: lot.lotId, + qty: submitQty, + status: 'available', + operation: 'pick' + }); + } + + // REMOVED: Don't check completion here - do it after all submissions + // Return the lot info for completion check + return { + success: true, + lotNo: lot.lotNo, + pickOrderConsoCode: lot.pickOrderConsoCode, + newStatus: newStatus + }; + }); + + // Wait for all submissions to complete + const results = await Promise.all(submitPromises); + const successCount = results.filter(r => r.success).length; + + console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); + + // FIXED: Check completion AFTER all submissions are done + // Collect unique consoCodes from completed lots + const completedConsoCodes = new Set(); + results.forEach(result => { + if (result.success && result.newStatus === 'completed' && result.pickOrderConsoCode) { + completedConsoCodes.add(result.pickOrderConsoCode); + } + }); + + // Check completion for each unique consoCode + await Promise.all( + Array.from(completedConsoCodes).map(async (consoCode) => { + try { + console.log(`🔍 Checking completion for pick order: ${consoCode}`); + const completionResponse = await checkAndCompletePickOrderByConsoCode(consoCode); + console.log(` Pick order completion check result for ${consoCode}:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(`✅ Pick order ${consoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order ${consoCode} not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion for ${consoCode}: ${completionResponse.message}`); + } + } catch (error) { + console.error(`❌ Error checking pick order completion for ${consoCode}:`, error); + } + })); + + // Refresh data once after all submissions and completion checks + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + + if (successCount > 0) { + setQrScanSuccess(true); + setTimeout(() => { + setQrScanSuccess(false); + checkAndAutoAssignNext(); + }, 2000); + } + + } catch (error) { + console.error("Error submitting all scanned items:", error); + setQrScanError(true); + } finally { + setIsSubmittingAll(false); + } + }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); + + // Calculate scanned items count + const scannedItemsCount = useMemo(() => { + return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; + }, [combinedLotData]); + // Handle reject lot + const handleRejectLot = useCallback(async (lot: any) => { + if (!lot.stockOutLineId) { + console.error("No stock out line found for this lot"); + return; + } + + try { + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'rejected', + qty: 0 + }); + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + console.log("Lot rejected successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + } catch (error) { + console.error("Error rejecting lot:", error); + } + }, [fetchJobOrderData, checkAndAutoAssignNext]); + + // Handle pick execution form + const handlePickExecutionForm = useCallback((lot: any) => { + console.log("=== Pick Execution Form ==="); + console.log("Lot data:", lot); + + if (!lot) { + console.warn("No lot data provided for pick execution form"); + return; + } + + console.log("Opening pick execution form for lot:", lot.lotNo); + + setSelectedLotForExecutionForm(lot); + setPickExecutionFormOpen(true); + + console.log("Pick execution form opened for lot ID:", lot.lotId); + }, []); + + const handlePickExecutionFormSubmit = useCallback(async (data: any) => { + try { + console.log("Pick execution form submitted:", data); + const issueData = { + ...data, + type: "Jo", // Delivery Order Record 类型 + }; + + const result = await recordPickExecutionIssue(issueData); + console.log("Pick execution issue recorded:", result); + + if (result && result.code === "SUCCESS") { + console.log(" Pick execution issue recorded successfully"); + } else { + console.error("❌ Failed to record pick execution issue:", result); + } + + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + } catch (error) { + console.error("Error submitting pick execution form:", error); + } + }, [fetchJobOrderData]); + + // Calculate remaining required quantity + const calculateRemainingRequiredQty = useCallback((lot: any) => { + const requiredQty = lot.requiredQty || 0; + const stockOutLineQty = lot.stockOutLineQty || 0; + return Math.max(0, requiredQty - stockOutLineQty); + }, []); + + // Search criteria + const searchCriteria: Criterion[] = [ + { + label: t("Pick Order Code"), + paramName: "pickOrderCode", + type: "text", + }, + { + label: t("Item Code"), + paramName: "itemCode", + type: "text", + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { + label: t("Lot No"), + paramName: "lotNo", + type: "text", + }, + ]; + + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + console.log("Search query:", query); + + if (!originalCombinedData) return; + + const filtered = originalCombinedData.filter((lot: any) => { + const pickOrderCodeMatch = !query.pickOrderCode || + lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + + const itemCodeMatch = !query.itemCode || + lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); + + const itemNameMatch = !query.itemName || + lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); + + const lotNoMatch = !query.lotNo || + lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase()); + + return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch; + }); + + setCombinedLotData(filtered); + console.log("Filtered lots count:", filtered.length); + }, [originalCombinedData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + if (originalCombinedData) { + setCombinedLotData(originalCombinedData); + } + }, [originalCombinedData]); + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setPaginationController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setPaginationController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); + + // Pagination data with sorting by routerIndex + const paginatedData = useMemo(() => { + // Sort by routerIndex first, then by other criteria + const sortedData = [...combinedLotData].sort((a, b) => { + const aIndex = a.routerIndex || 0; + const bIndex = b.routerIndex || 0; + + // Primary sort: by routerIndex + if (aIndex !== bIndex) { + return aIndex - bIndex; + } + + // Secondary sort: by pickOrderCode if routerIndex is the same + if (a.pickOrderCode !== b.pickOrderCode) { + return a.pickOrderCode.localeCompare(b.pickOrderCode); + } + + // Tertiary sort: by lotNo if everything else is the same + return (a.lotNo || '').localeCompare(b.lotNo || ''); + }); + + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return sortedData.slice(startIndex, endIndex); + }, [combinedLotData, paginationController]); + + // Add these functions for manual scanning + const handleStartScan = useCallback(() => { + console.log(" Starting manual QR scan..."); + setIsManualScanning(true); + setProcessedQrCodes(new Set()); + setLastProcessedQr(''); + setQrScanError(false); + setQrScanSuccess(false); + startScan(); + }, [startScan]); + + const handleStopScan = useCallback(() => { + console.log("⏹️ Stopping manual QR scan..."); + setIsManualScanning(false); + setQrScanError(false); + setQrScanSuccess(false); + stopScan(); + resetScan(); + }, [stopScan, resetScan]); + useEffect(() => { + return () => { + // Cleanup when component unmounts (e.g., when switching tabs) + if (isManualScanning) { + console.log("🧹 Component unmounting, stopping QR scanner..."); + stopScan(); + resetScan(); + } + }; + }, [isManualScanning, stopScan, resetScan]); + useEffect(() => { + if (isManualScanning && combinedLotData.length === 0) { + console.log("⏹️ No data available, auto-stopping QR scan..."); + handleStopScan(); + } + }, [combinedLotData.length, isManualScanning, handleStopScan]); + + // Cleanup effect + useEffect(() => { + return () => { + // Cleanup when component unmounts (e.g., when switching tabs) + if (isManualScanning) { + console.log("🧹 Component unmounting, stopping QR scanner..."); + stopScan(); + resetScan(); + } + }; + }, [isManualScanning, stopScan, resetScan]); + const getStatusMessage = useCallback((lot: any) => { + switch (lot.stockOutLineStatus?.toLowerCase()) { + case 'pending': + return t("Please finish QR code scan and pick order."); + case 'checked': + return t("Please submit the pick order."); + case 'partially_completed': + return t("Partial quantity submitted. Please submit more or complete the order."); + case 'completed': + return t("Pick order completed successfully!"); + case 'rejected': + return t("Lot has been rejected and marked as unavailable."); + case 'unavailable': + return t("This order is insufficient, please pick another lot."); + default: + return t("Please finish QR code scan and pick order."); + } + }, [t]); + + return ( + ( + lot.lotAvailability !== 'rejected' && + lot.stockOutLineStatus !== 'rejected' && + lot.stockOutLineStatus !== 'completed' + )} + > + + + {/* Job Order Header */} + {jobOrderData && ( + + + + {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.code || '-'} + + + {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} + + + {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} + + + + + )} + + + {/* Combined Lot Table */} + + + + + + {!isManualScanning ? ( + + ) : ( + + )} + {/* ADD THIS: Submit All Scanned Button */} + + + + + + {qrScanError && !qrScanSuccess && ( + + {t("QR code does not match any item in current orders.")} + + )} + {qrScanSuccess && ( + + {t("QR code verified.")} + + )} + + + + + + {t("Index")} + {t("Route")} + {t("Item Code")} + {t("Item Name")} + {t("Lot No")} + {t("Lot Required Pick Qty")} + {t("Scan Result")} + {t("Submit Required Pick Qty")} + + + + {paginatedData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedData.map((lot, index) => ( + + + + {index + 1} + + + + + {lot.routerRoute || '-'} + + + {lot.itemCode} + {lot.itemName+'('+lot.uomDesc+')'} + + + + {lot.lotNo} + + + + + {(() => { + const requiredQty = lot.requiredQty || 0; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; + })()} + + + + {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( + + + + ) : null} + + + + + + + + + + + + + )) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> +
+
+ + {/* QR Code Modal */} + {!lotConfirmationOpen && ( + { + setQrModalOpen(false); + setSelectedLotForQr(null); + stopScan(); + resetScan(); + }} + lot={selectedLotForQr} + combinedLotData={combinedLotData} + onQrCodeSubmit={handleQrCodeSubmitFromModal} + /> + )} + {/* Add Lot Confirmation Modal */} + {lotConfirmationOpen && expectedLotData && scannedLotData && ( + { + setLotConfirmationOpen(false); + setExpectedLotData(null); + setScannedLotData(null); + setSelectedLotForQr(null); + + }} + onConfirm={handleLotConfirmation} + expectedLot={expectedLotData} + scannedLot={scannedLotData} + isLoading={isConfirmingLot} + /> + )} + {/* Pick Execution Form Modal */} + {pickExecutionFormOpen && selectedLotForExecutionForm && ( + { + setPickExecutionFormOpen(false); + setSelectedLotForExecutionForm(null); + }} + onSubmit={handlePickExecutionFormSubmit} + selectedLot={selectedLotForExecutionForm} + selectedPickOrderLine={{ + id: selectedLotForExecutionForm.pickOrderLineId, + itemId: selectedLotForExecutionForm.itemId, + itemCode: selectedLotForExecutionForm.itemCode, + itemName: selectedLotForExecutionForm.itemName, + pickOrderCode: selectedLotForExecutionForm.pickOrderCode, + // Add missing required properties from GetPickOrderLineInfo interface + availableQty: selectedLotForExecutionForm.availableQty || 0, + requiredQty: selectedLotForExecutionForm.requiredQty || 0, + uomCode: selectedLotForExecutionForm.uomCode || '', + uomDesc: selectedLotForExecutionForm.uomDesc || '', + pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty + suggestedList: [] // Add required suggestedList property + }} + pickOrderId={selectedLotForExecutionForm.pickOrderId} + pickOrderCreateDate={new Date()} + /> + )} +
+
+ ); +}; + +export default JobPickExecution \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 38c53a7..3695359 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -196,11 +196,13 @@ const NavigationContent: React.FC = () => { label: "Detail Scheduling", path: "/scheduling/detailed", }, + /* { icon: , label: "Production", path: "/production", }, + */ ], }, { @@ -218,6 +220,11 @@ const NavigationContent: React.FC = () => { label: "Job Order Pickexcution", path: "/jodetail", }, + { + icon: , + label: "Job Order Production Process", + path: "/productionProcess", + }, ], }, { diff --git a/src/components/PickOrderSearch/LotTable.tsx b/src/components/PickOrderSearch/LotTable.tsx index 3ff86f2..61aa7c6 100644 --- a/src/components/PickOrderSearch/LotTable.tsx +++ b/src/components/PickOrderSearch/LotTable.tsx @@ -303,7 +303,7 @@ const QrCodeModal: React.FC<{ {/* Manual Input with Submit-Triggered Helper Text */} - {false &&( + {true &&( {t("Manual Input")}: @@ -588,7 +588,44 @@ const LotTable: React.FC = ({ console.error("Error submitting pick execution form:", error); } }, [onDataRefresh, onLotDataRefresh]); - + const allLotsUnavailable = useMemo(() => { + if (!paginatedLotTableData || paginatedLotTableData.length === 0) return false; + return paginatedLotTableData.every((lot) => + ['rejected', 'expired', 'insufficient_stock', 'status_unavailable'] + .includes(lot.lotAvailability) + ); + }, [paginatedLotTableData]); + + // 完成当前行(无可用批次)的点击处理 + const handleCompleteWithoutLot = useCallback(async (lot: LotPickData) => { + try { + if (!lot.stockOutLineId) { + alert("No stock out line for this lot. Please contact administrator."); + return; + } + + // 这里建议调用你自己在 actions 里封装的 API,例如: + // await completeStockOutLineWithoutLot(lot.stockOutLineId); + + // 简单点可以复用 updateStockOutLineStatus,直接标记 COMPLETE、数量为 0: + await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'completed', + qty: lot.stockOutLineQty || 0, + }); + + // 刷新数据 + if (onLotDataRefresh) { + await onLotDataRefresh(); + } + if (onDataRefresh) { + await onDataRefresh(); + } + } catch (e) { + console.error("Error completing stock out line without lot", e); + alert("Failed to complete this line. Please try again."); + } + }, [onDataRefresh, onLotDataRefresh]); return ( <> diff --git a/src/components/ProductionProcess/ProcessSummaryHeader.tsx b/src/components/ProductionProcess/ProcessSummaryHeader.tsx new file mode 100644 index 0000000..412f7cf --- /dev/null +++ b/src/components/ProductionProcess/ProcessSummaryHeader.tsx @@ -0,0 +1,46 @@ +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"; + +interface Props { + processData?: { + jobOrderCode?: string; + itemCode?: string; + itemName?: string; + jobType?: string; + outputQty?: number | string; + date?: string; + }; +} + +const ProcessSummaryHeader: React.FC = ({ processData }) => { + const { t } = useTranslation(); + return ( + + + + + {t("Job Order Code")}: {processData?.jobOrderCode} + + + {t("Item")}: {processData?.itemCode+"-"+processData?.itemName} + + + + {t("Job Type")}: {t(processData?.jobType ?? "")} + + + + {t("Qty")}: {processData?.outputQty} + + + {t("Production Date")}: {processData?.date ? dayjs(processData.date).format(OUTPUT_DATE_FORMAT) : ""} + + + + + ); +}; + +export default ProcessSummaryHeader; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 65f64db..3a5fe7a 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -49,15 +49,17 @@ import { import { fetchNameList, NameList } from "@/app/api/user/actions"; import ProductionProcessStepExecution from "./ProductionProcessStepExecution"; import ProductionOutputFormPage from "./ProductionOutputFormPage"; - +import ProcessSummaryHeader from "./ProcessSummaryHeader"; interface ProductProcessDetailProps { jobOrderId: number; onBack: () => void; + fromJosave?: boolean; } const ProductionProcessDetail: React.FC = ({ jobOrderId, onBack, + fromJosave, }) => { const { t } = useTranslation(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -78,6 +80,8 @@ const ProductionProcessDetail: React.FC = ({ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [scannedOperatorId, setScannedOperatorId] = useState(null); const [scannedEquipmentId, setScannedEquipmentId] = useState(null); + const [scannedEquipmentTypeSubTypeEquipmentNo, setScannedEquipmentTypeSubTypeEquipmentNo] = useState(null); + const [scannedStaffNo, setScannedStaffNo] = useState(null); const [scanningLineId, setScanningLineId] = useState(null); const [lineDetailForScan, setLineDetailForScan] = useState(null); const [showScanDialog, setShowScanDialog] = useState(false); @@ -146,6 +150,7 @@ const ProductionProcessDetail: React.FC = ({ // 提交产出数据 + /* const processQrCode = useCallback((qrValue: string, lineId: number) => { // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) if (qrValue.match(/\{2fitestu(\d+)\}/)) { @@ -205,7 +210,92 @@ const ProductionProcessDetail: React.FC = ({ // TODO: 处理普通文本格式 } }, []); +*/ +// 提交产出数据 +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}號`; + setScannedEquipmentTypeSubTypeEquipmentNo(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 + // 注意:lineDetail 的结构可能不同,需要根据实际 API 响应调整 + const equipmentName = (lineDetail as any).equipment || (lineDetail as any).equipmentType || ""; + if (equipmentName) { + const equipmentTypeSubTypeEquipmentNo = `${equipmentName}-${equipmentNo}號`; + setScannedEquipmentTypeSubTypeEquipmentNo(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); + }); + } + return; + } + + + // 员工编号格式:{2fitestu任何内容} - 直接作为 staffNo + // 例如:{2fitestu123} = staffNo: "123" + // 例如:{2fitestustaff001} = staffNo: "staff001" + if (qrValue.match(/\{2fitestu(.+)\}/)) { + const match = qrValue.match(/\{2fitestu(.+)\}/); + const staffNo = match![1]; + setScannedStaffNo(staffNo); + return; + } + // 正常 QR 扫描器扫描格式 + const trimmedValue = qrValue.trim(); + + // 检查 staffNo 格式:"staffNo: STAFF001" 或 "staffNo:STAFF001" + const staffNoMatch = trimmedValue.match(/^staffNo:\s*(.+)$/i); + if (staffNoMatch) { + const staffNo = staffNoMatch[1].trim(); + setScannedStaffNo(staffNo); + return; + } + + // 检查 equipmentTypeSubTypeEquipmentNo 格式 + const equipmentCodeMatch = trimmedValue.match(/^(?:equipmentTypeSubTypeEquipmentNo|EquipmentType-SubType-EquipmentNo):\s*(.+)$/i); + if (equipmentCodeMatch) { + const equipmentCode = equipmentCodeMatch[1].trim(); + setScannedEquipmentTypeSubTypeEquipmentNo(equipmentCode); + return; + } + + // 其他格式处理(JSON、普通文本等) + try { + const qrData = JSON.parse(qrValue); + // TODO: 处理 JSON 格式的 QR 码 + } catch { + // 普通文本格式 - 尝试判断是 staffNo 还是 equipmentCode + if (trimmedValue.length > 0) { + if (trimmedValue.toUpperCase().startsWith("STAFF") || /^\d+$/.test(trimmedValue)) { + // 可能是员工编号 + setScannedStaffNo(trimmedValue); + } else if (trimmedValue.includes("-")) { + // 可能包含 "-" 的是设备代码(如 "包裝機類-真空八爪魚機-1號") + setScannedEquipmentTypeSubTypeEquipmentNo(trimmedValue); + } + } + } +}, [lines]); // 处理 QR 码扫描效果 useEffect(() => { if (isManualScanning && qrValues.length > 0 && scanningLineId) { @@ -219,74 +309,72 @@ const ProductionProcessDetail: React.FC = ({ processQrCode(latestQr, scanningLineId); } }, [qrValues, isManualScanning, scanningLineId, processedQrCodes, processQrCode]); + + const submitScanAndStart = useCallback(async (lineId: number) => { console.log("submitScanAndStart called with:", { lineId, - scannedOperatorId, - scannedEquipmentId, + scannedStaffNo, + scannedEquipmentTypeSubTypeEquipmentNo, }); - - if (!scannedOperatorId) { - console.log("No operatorId, cannot submit"); + + if (!scannedStaffNo) { + console.log("No staffNo, cannot submit"); setIsAutoSubmitting(false); - return false; // 没有 operatorId,不能提交 + return false; // 没有 staffNo,不能提交 } - + try { // 获取 line detail 以检查 bomProcessEquipmentId const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); - // 提交 operatorId 和 equipmentId + // 提交 staffNo 和 equipmentTypeSubTypeEquipmentNo console.log("Submitting scan data:", { productProcessLineId: lineId, - operatorId: scannedOperatorId, - equipmentId: scannedEquipmentId || undefined, + staffNo: scannedStaffNo, + equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo, }); - + const response = await updateProductProcessLineQrscan({ productProcessLineId: lineId, - operatorId: scannedOperatorId, - equipmentId: scannedEquipmentId || undefined, + equipmentTypeSubTypeEquipmentNo: scannedEquipmentTypeSubTypeEquipmentNo || undefined, + staffNo: scannedStaffNo || undefined, }); - + console.log("Scan submit response:", response); - + // 检查响应中的 message 字段来判断是否成功 - // 如果后端返回 message 不为 null,说明验证失败 if (response && response.message) { setIsAutoSubmitting(false); - // 清除定时器 if (autoSubmitTimerRef.current) { clearTimeout(autoSubmitTimerRef.current); autoSubmitTimerRef.current = null; } - //alert(response.message || t("Validation failed. Please check operator and equipment.")); - return false; - - } - // 验证通过,继续执行后续步骤 - console.log("Validation passed, starting line..."); - handleStopScan(); - setShowScanDialog(false); - setIsAutoSubmitting(false); - - await handleStartLine(lineId); - setSelectedLineId(lineId); - setIsExecutingLine(true); - await fetchProcessDetail(); - - return true; - } catch (error) { - console.error("Error submitting scan:", error); - //alert(t("Failed to submit scan data. Please try again.")); - setIsAutoSubmitting(false); return false; } - }, [scannedOperatorId, scannedEquipmentId, lineDetailForScan, t, fetchProcessDetail]); + + // 验证通过,继续执行后续步骤 + console.log("Validation passed, starting line..."); + handleStopScan(); + setShowScanDialog(false); + setIsAutoSubmitting(false); + + await handleStartLine(lineId); + setSelectedLineId(lineId); + setIsExecutingLine(true); + await fetchProcessDetail(); + + return true; + } catch (error) { + console.error("Error submitting scan:", error); + setIsAutoSubmitting(false); + return false; + } + }, [scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, lineDetailForScan, t, fetchProcessDetail]); const handleSubmitScanAndStart = useCallback(async (lineId: number) => { console.log("handleSubmitScanAndStart called with lineId:", lineId); - if (!scannedOperatorId) { + if (!scannedStaffNo) { //alert(t("Please scan operator code first")); return; } @@ -316,8 +404,11 @@ const ProductionProcessDetail: React.FC = ({ setLineDetailForScan(null); // 获取 line detail 以获取 bomProcessEquipmentId fetchProductProcessLineDetail(lineId) - .then(setLineDetailForScan) - .catch(err => console.error("Failed to load line detail", err)); + .then(setLineDetailForScan) + .catch(err => { + console.error("Failed to load line detail", err); + // 不阻止扫描继续,line detail 不是必需的 + }); startScan(); }, [startScan]); @@ -351,16 +442,16 @@ const ProductionProcessDetail: React.FC = ({ useEffect(() => { console.log("Auto-submit check:", { scanningLineId, - scannedOperatorId, - scannedEquipmentId, + scannedStaffNo, + scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, }); if ( scanningLineId && - scannedOperatorId !== null && - scannedEquipmentId !== null && + scannedStaffNo !== null && + scannedEquipmentTypeSubTypeEquipmentNo !== null && !isAutoSubmitting && isManualScanning ) { @@ -385,7 +476,7 @@ const ProductionProcessDetail: React.FC = ({ // 注意:这里不立即清除定时器,因为我们需要它执行 // 只在组件卸载时清除 }; - }, [scanningLineId, scannedOperatorId, scannedEquipmentId, isAutoSubmitting, isManualScanning, submitScanAndStart]); + }, [scanningLineId, scannedStaffNo, scannedEquipmentTypeSubTypeEquipmentNo, isAutoSubmitting, isManualScanning, submitScanAndStart]); useEffect(() => { return () => { if (autoSubmitTimerRef.current) { @@ -477,7 +568,7 @@ const ProductionProcessDetail: React.FC = ({ {t("Production Process Steps")} - + {!isExecutingLine ? ( /* ========== 步骤列表视图 ========== */ @@ -509,7 +600,8 @@ const ProductionProcessDetail: React.FC = ({ {t("Status")} - {t("Action")} + + {!fromJosave&&({t("Action")})} @@ -529,7 +621,7 @@ const ProductionProcessDetail: React.FC = ({ {line.name} {line.description || "-"} - {equipmentName} + {line.equipmentDetailCode||equipmentName} {line.operatorName} {/* {line.durationInMinutes} @@ -561,6 +653,7 @@ const ProductionProcessDetail: React.FC = ({ )} + {!fromJosave&&( {statusLower === 'pending' ? ( )} + )} ); })} @@ -635,17 +729,17 @@ const ProductionProcessDetail: React.FC = ({ - {scannedOperatorId - ? `${t("Operator")}: ${scannedOperatorId}` - : t("Please scan operator code") + {scannedStaffNo + ? `${t("Staff No")}: ${scannedStaffNo}` + : t("Please scan staff no") } - {scannedEquipmentId - ? `${t("Equipment")}: ${scannedEquipmentId}` + {scannedEquipmentTypeSubTypeEquipmentNo + ? `${t("Equipment Type/Code")}: ${scannedEquipmentTypeSubTypeEquipmentNo}` : t("Please scan equipment code (optional if not required)") } @@ -672,7 +766,7 @@ const ProductionProcessDetail: React.FC = ({ diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index d9a29e6..79aa8a9 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -17,7 +17,7 @@ import { } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; -import { fetchProductProcessesByJobOrderId } from "@/app/api/jo/actions"; +import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; @@ -30,6 +30,7 @@ import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { releaseJo, startJo } from "@/app/api/jo/actions"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; +import ProcessSummaryHeader from "./ProcessSummaryHeader"; interface JobOrderLine { id: number; jobOrderId: number; @@ -127,7 +128,12 @@ const stockCounts = useMemo(() => { }; }, [jobOrderLines, inventoryData]); const status = processData?.status?.toLowerCase?.() ?? ""; - +const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { + const response = await deleteJobOrder(jobOrderId) + if (response) { + setProcessData(response.entity); + } +}, [jobOrderId]); const handleRelease = useCallback(async ( jobOrderId: number) => { // TODO: 替换为实际的 release 调用 console.log("Release clicked for jobOrderId:", jobOrderId); @@ -256,6 +262,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { align: "left", headerAlign: "center", type: "number", + renderCell: (params) => { + return {params.value}; + }, }, { field: "description", @@ -263,6 +272,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { flex: 1, align: "left", headerAlign: "center", + renderCell: (params) => { + return {params.value || ""}; + }, }, ]; const productionProcessesLineRemarkTableRows = @@ -270,6 +282,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { id: line.seqNo, seqNo: line.seqNo, description: line.description ?? "", + })) ?? []; @@ -356,11 +369,13 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { const pickTableRows = jobOrderLines.map((line, index) => ({ ...line, - id: line.id || index, + //id: line.id || index, + id: index + 1, })); const PickTableContent = () => ( + { {t("Lines with insufficient stock: ")}{stockCounts.insufficient} - {fromJosave && ( + {fromJosave && ( + + )} + {fromJosave && ( - + {/* 标签页 */} @@ -455,7 +482,9 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { onBack={() => { // 切换回第一个标签页,或者什么都不做 setTabIndex(0); + }} + fromJosave={fromJosave} /> )} {tabIndex === 3 && } diff --git a/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx b/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx index b3aed36..d9308ca 100644 --- a/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx +++ b/src/components/QrCodeScannerProvider/QrCodeScannerProvider.tsx @@ -166,21 +166,41 @@ const QrCodeScannerProvider: React.FC = ({ if (qrCodeScannerValues.length > 0) { const scannedValues = qrCodeScannerValues[0]; console.log("%c Scanned Result: ", "color:cyan", scannedValues); - + if (scannedValues.substring(0, 8) == "{2fitest") { // DEBUGGING - const number = scannedValues.substring(8, scannedValues.length - 1); - if (/^\d+$/.test(number)) { // Check if number contains only digits - console.log("%c DEBUG: detected ID: ", "color:pink", number); - const debugValue = { - value: number - } - setScanResult(debugValue); - } else { - resetQrCodeScanner("DEBUG -- Invalid number format: " + number); + // 先检查是否是 {2fiteste...} 或 {2fitestu...} 格式 + // 这些格式需要传递完整值给 processQrCode 处理 + if (scannedValues.length > 9) { + const ninthChar = scannedValues.substring(8, 9); + if (ninthChar === "e" || ninthChar === "u") { + // {2fiteste数字} 或 {2fitestu任何内容} 格式 + console.log("%c DEBUG: detected shortcut format: ", "color:pink", scannedValues); + const debugValue = { + value: scannedValues // 传递完整值,让 processQrCode 处理 + } + setScanResult(debugValue); + return; + } + } + + // 原有的 {2fitest数字} 格式(纯数字,向后兼容) + const number = scannedValues.substring(8, scannedValues.length - 1); + if (/^\d+$/.test(number)) { // Check if number contains only digits + console.log("%c DEBUG: detected ID: ", "color:pink", number); + const debugValue = { + value: number } - return; + setScanResult(debugValue); + } else { + // 如果不是纯数字,传递完整值让 processQrCode 处理 + const debugValue = { + value: scannedValues + } + setScanResult(debugValue); + } + return; } - + try { const data: QrCodeInfo = JSON.parse(scannedValues); console.log("%c Parsed scan data", "color:green", data); @@ -188,18 +208,18 @@ const QrCodeScannerProvider: React.FC = ({ const content = scannedValues.substring(1, scannedValues.length - 1); data.value = content; setScanResult(data); - - } catch (error) { // Rought match for other scanner input -- Pending Review + + } catch (error) { // Rough match for other scanner input -- Pending Review const silId = findIdByRoughMatch(scannedValues, "StockInLine").number ?? 0; if (silId == 0) { const whId = findIdByRoughMatch(scannedValues, "warehouseId").number ?? 0; setScanResult({...scanResult, stockInLineId: whId, value: whId.toString()}); } else { setScanResult({...scanResult, stockInLineId: silId, value: silId.toString()}); } - + resetQrCodeScanner(String(error)); } - + // resetQrCodeScanner(); } }, [qrCodeScannerValues]); diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index d94dbfa..d173a65 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -2,7 +2,8 @@ "dashboard": "資訊展示面板", "Edit": "編輯", - + "Job Order Production Process": "工單生產流程", + "productionProcess": "生產流程", "Search Criteria": "搜尋條件", "All": "全部", "No options": "沒有選項", @@ -12,11 +13,12 @@ "code": "編號", "Name": "名稱", "Type": "類型", - + "WIP": "半成品", "R&D": "研發", "STF": "樣品", "Other": "其他", + "Add some entries!": "添加條目", "Add Record": "新增", "Clean Record": "重置", @@ -54,12 +56,15 @@ "sfg": "半成品", "item": "貨品", "FG":"成品", + "Qty":"數量", "FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", "View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", "Delivery Order":"送貨訂單", "Detail Scheduling":"詳細排程", "Customer":"客戶", "qcItem":"品檢項目", + "Item":"物料", + "Production Date":"生產日期", "QC Check Item":"QC品檢項目", "QC Category":"QC品檢模板", "qcCategory":"品檢模板", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index fa1d0e0..1551c2b 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -12,6 +12,7 @@ "UoM": "銷售單位", "Status": "工單狀態", "Lot No.": "批號", + "Delete Job Order": "刪除工單", "Bom": "半成品/成品編號", "Release": "放單", "Pending": "待掃碼", @@ -276,10 +277,11 @@ "success": "成功", "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", "BOM Status": "材料預備狀況", - "Estimated Production Date": "預計生產日期及時間", + "Estimated Production Date": "預計生產日期", "Plan Start": "預計生產日期", - "Plan Start From": "預計生產日期及時間", - "Plan Start To": "預計生產日期及時間至", + "Plan Start From": "預計生產日期", + "Delivery Note Code": "送貨單編號", + "Plan Start To": "預計生產日期至", "By-product": "副產品", "Complete Step": "完成步驟", "Defect": "缺陷", @@ -329,7 +331,8 @@ "Total Steps": "總步驟數", "Unknown": "", "Job Type": "工單類型", - + "Production Date":"生產日期", + "Jo Pick Order Detail":"工單提料詳情", "WIP": "半成品", "R&D": "研發", "STF": "員工餐", diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index a7ce0ac..2382db7 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -204,6 +204,7 @@ "Report and Pick another lot": "上報並需重新選擇批號", "Accept Stock Out": "接受出庫", "Pick Another Lot": "欠數,並重新選擇批號", + "Delivery Note Code": "送貨單編號", "Lot No": "批號", "Expiry Date": "到期日", "Location": "位置",