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 */}
+ {/* }
+ LinkComponent={Link}
+ href="/productionProcess/create"
+ >
+ {t("Create Process")}
+ */}
+
+
+
+
+ >
+ );
+};
+
+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 (
+
+
+ }>
+ {t("Back to List")}
+
+
+
+
+
+ );
+};
+
+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 ? (
+ }
+ onClick={handleStartScan}
+ color="primary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Start QR Scan")}
+
+ ) : (
+ }
+ onClick={handleStopScan}
+ color="secondary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Stop QR Scan")}
+
+ )}
+ {/* 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": "位置",