| @@ -11,7 +11,7 @@ function checkMissingTranslations(sourceFile, jsonFile) { | |||
| // 读取翻译 JSON 文件 | |||
| const translations = JSON.parse(fs.readFileSync(jsonFile, 'utf-8')); | |||
| // ✅ 只匹配 t('...') 和 t("...") 和 t(`...`),不包含模板变量 | |||
| // 只匹配 t('...') 和 t("...") 和 t(`...`),不包含模板变量 | |||
| const tRegex = /\bt\(["`']([^"`'${}]+)["`']\)/g; | |||
| const matches = [...sourceCode.matchAll(tRegex)]; | |||
| @@ -86,7 +86,7 @@ if (args.length === 0) { | |||
| console.log(' node check-translations.js src/components/Jodetail/JodetailSearch.tsx src/i18n/zh/jo.json'); | |||
| console.log(' node check-translations.js --dir src/components/Jodetail src/i18n/zh/jo.json'); | |||
| console.log('\n注意:'); | |||
| console.log(' ✅ 只检查 t("key") 调用'); | |||
| console.log(' 只检查 t("key") 调用'); | |||
| console.log(' ❌ 忽略 alert(), console.log() 等普通字符串'); | |||
| console.log(' ❌ 忽略模板字符串中的 ${} 变量部分'); | |||
| process.exit(0); | |||
| @@ -103,7 +103,7 @@ if (args[0] === '--dir') { | |||
| const { results, totalMissing } = checkDirectory(directory, jsonFile); | |||
| if (Object.keys(results).length === 0) { | |||
| console.log('✅ 太棒了!没有发现缺失的翻译键!'); | |||
| console.log(' 太棒了!没有发现缺失的翻译键!'); | |||
| } else { | |||
| console.log(`⚠️ 发现 ${Object.keys(results).length} 个文件有缺失的翻译键\n`); | |||
| @@ -153,7 +153,7 @@ if (args[0] === '--dir') { | |||
| }); | |||
| console.log('─'.repeat(60)); | |||
| } else { | |||
| console.log('\n✅ 太棒了!所有使用的翻译键都已定义!'); | |||
| console.log('\n 太棒了!所有使用的翻译键都已定义!'); | |||
| } | |||
| if (result.unusedKeys.length > 0 && result.unusedKeys.length <= 20) { | |||
| @@ -1,4 +1,4 @@ | |||
| import ProductionProcess from "../../../components/ProductionProcess"; | |||
| import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage"; | |||
| import { getServerI18n } from "../../../i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| @@ -15,7 +15,6 @@ export const metadata: Metadata = { | |||
| const production: React.FC = async () => { | |||
| const { t } = await getServerI18n("claims"); | |||
| // preloadClaims(); | |||
| return ( | |||
| <> | |||
| @@ -26,22 +25,22 @@ const production: React.FC = async () => { | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Production")} | |||
| {t("Production Process")} | |||
| </Typography> | |||
| <Button | |||
| {/* Optional: Remove or modify create button, because creation is done via API automatically */} | |||
| {/* <Button | |||
| variant="contained" | |||
| startIcon={<Add />} | |||
| LinkComponent={Link} | |||
| href="/production/create" | |||
| > | |||
| {t("Create Claim")} | |||
| </Button> | |||
| {t("Create Process")} | |||
| </Button> */} | |||
| </Stack> | |||
| {/* <Suspense fallback={<ClaimSearch.Loading />}> */} | |||
| <ProductionProcess /> | |||
| {/* </Suspense> */} | |||
| <ProductionProcessPage /> {/* Use new component */} | |||
| </> | |||
| ); | |||
| }; | |||
| export default production; | |||
| @@ -129,13 +129,13 @@ export const recordSecondScanIssue = cache(async ( | |||
| itemId: number, | |||
| data: { | |||
| qty: number; // verified qty (actual pick qty) | |||
| missQty?: number; // ✅ 添加:miss qty | |||
| badItemQty?: number; // ✅ 添加:bad item qty | |||
| missQty?: number; // 添加:miss qty | |||
| badItemQty?: number; // 添加:bad item qty | |||
| isMissing: boolean; | |||
| isBad: boolean; | |||
| reason: string; | |||
| createdBy: number; | |||
| type?: string; // ✅ type 也应该是可选的 | |||
| type?: string; // type 也应该是可选的 | |||
| } | |||
| ) => { | |||
| @@ -188,6 +188,136 @@ export interface ProductProcessWithLinesResponse { | |||
| date: string; | |||
| lines: ProductProcessLineResponse[]; | |||
| } | |||
| export interface UpdateProductProcessLineQtyRequest { | |||
| productProcessLineId: number; | |||
| outputFromProcessQty: number; | |||
| outputFromProcessUom: string; | |||
| defectQty: number; | |||
| defectUom: string; | |||
| scrapQty: number; | |||
| scrapUom: string; | |||
| } | |||
| export interface UpdateProductProcessLineQtyResponse { | |||
| id: number; | |||
| outputFromProcessQty: number; | |||
| outputFromProcessUom: string; | |||
| defectQty: number; | |||
| defectUom: string; | |||
| scrapQty: number; | |||
| scrapUom: string; | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| } | |||
| export interface AllProductProcessResponse { | |||
| id: number; | |||
| productProcessCode: string; | |||
| status: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| date: string; | |||
| bomId?: number; | |||
| } | |||
| export interface AllJoborderProductProcessInfoResponse { | |||
| id: number; | |||
| productProcessCode: string; | |||
| status: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| date: string; | |||
| bomId?: number; | |||
| itemName: string; | |||
| jobOrderId: number; | |||
| jobOrderCode: string; | |||
| productProcessLineCount: number; | |||
| FinishedProductProcessLineCount: number; | |||
| lines: ProductProcessInfoResponse[]; | |||
| } | |||
| export interface ProductProcessInfoResponse { | |||
| id: number; | |||
| operatorId?: number; | |||
| operatorName?: string; | |||
| equipmentId?: number; | |||
| equipmentName?: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| status: string; | |||
| } | |||
| export interface ProductProcessLineQrscanUpadteRequest { | |||
| lineId: number; | |||
| operatorId?: number; | |||
| equipmentId?: number; | |||
| } | |||
| export interface ProductProcessLineDetailResponse { | |||
| id: number, | |||
| productProcessId: number, | |||
| bomProcessId: number, | |||
| operatorId: number, | |||
| equipmentType: string, | |||
| operatorName: string, | |||
| handlerId: number, | |||
| seqNo: number, | |||
| name: string, | |||
| description: string, | |||
| equipment: string, | |||
| startTime: string, | |||
| endTime: string, | |||
| defectQty: number, | |||
| defectUom: string, | |||
| scrapQty: number, | |||
| scrapUom: string, | |||
| byproductId: number, | |||
| byproductName: string, | |||
| byproductQty: number, | |||
| byproductUom: string | undefined, | |||
| } | |||
| export const fetchProductProcessLineDetailByJoid = cache(async (joid: number) => { | |||
| return serverFetchJson<ProductProcessLineDetailResponse>( | |||
| `${BASE_API_URL}/product-process/demo/joid/${joid}`, | |||
| { | |||
| method: "GET", | |||
| } | |||
| ); | |||
| }); | |||
| // /product-process/Demo/ProcessLine/detail/{lineId} | |||
| export const fetchProductProcessLineDetail = cache(async (lineId: number) => { | |||
| return serverFetchJson<ProductProcessLineDetailResponse>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`, | |||
| { | |||
| method: "GET", | |||
| } | |||
| ); | |||
| }); | |||
| export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/Demo/update`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(request), | |||
| } | |||
| ); | |||
| }); | |||
| export const fetchAllJoborderProductProcessInfo = cache(async () => { | |||
| return serverFetchJson<AllJoborderProductProcessInfoResponse[]>( | |||
| `${BASE_API_URL}/product-process/Demo/Process/all`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["productProcess"] }, | |||
| } | |||
| ); | |||
| }); | |||
| export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => { | |||
| return serverFetchJson<UpdateProductProcessLineQtyResponse>( | |||
| `${BASE_API_URL}/product-process/lines/${request.productProcessLineId}/update/qty`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(request), | |||
| } | |||
| ); | |||
| }; | |||
| export const startProductProcessLine = async (lineId: number, userId: number) => { | |||
| return serverFetchJson<ProductProcessLineResponse>( | |||
| `${BASE_API_URL}/product-process/lines/${lineId}/start?userId=${userId}`, | |||
| @@ -95,12 +95,12 @@ export interface GetPickOrderInfoResponse { | |||
| export interface GetPickOrderInfo { | |||
| id: number; | |||
| code: string; | |||
| consoCode: string | null; // ✅ 添加 consoCode 属性 | |||
| targetDate: string | number[]; // ✅ Support both formats | |||
| consoCode: string | null; // 添加 consoCode 属性 | |||
| targetDate: string | number[]; // Support both formats | |||
| type: string; | |||
| status: string; | |||
| assignTo: number; | |||
| groupName: string; // ✅ Add this field | |||
| groupName: string; // Add this field | |||
| pickOrderLines: GetPickOrderLineInfo[]; | |||
| } | |||
| @@ -256,16 +256,16 @@ export interface stockReponse{ | |||
| noLot: boolean; | |||
| } | |||
| export interface FGPickOrderResponse { | |||
| // ✅ 新增:支持多个 pick orders | |||
| // 新增:支持多个 pick orders | |||
| doPickOrderId: number; | |||
| pickOrderIds?: number[]; | |||
| pickOrderCodes?: string[]; // ✅ 改为数组 | |||
| pickOrderCodes?: string[]; // 改为数组 | |||
| deliveryOrderIds?: number[]; | |||
| deliveryNos?: string[]; // ✅ 改为数组 | |||
| deliveryNos?: string[]; // 改为数组 | |||
| numberOfPickOrders?: number; | |||
| lineCountsPerPickOrder?: number[];// ✅ 新增:pick order 数量 | |||
| lineCountsPerPickOrder?: number[];// 新增:pick order 数量 | |||
| // ✅ 保留原有字段用于向后兼容(显示第一个 pick order) | |||
| // 保留原有字段用于向后兼容(显示第一个 pick order) | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| pickOrderConsoCode: string; | |||
| @@ -332,17 +332,17 @@ export interface UpdateDoPickOrderHideStatusRequest { | |||
| } | |||
| export interface CompletedDoPickOrderResponse { | |||
| id: number; | |||
| doPickOrderRecordId: number; // ✅ 新增 | |||
| doPickOrderRecordId: number; // 新增 | |||
| pickOrderId: number; | |||
| pickOrderIds: number[]; // ✅ 新增:所有 pick order IDs | |||
| pickOrderIds: number[]; // 新增:所有 pick order IDs | |||
| pickOrderCode: string; | |||
| pickOrderCodes: string; // ✅ 新增:所有 pick order codes (逗号分隔) | |||
| pickOrderCodes: string; // 新增:所有 pick order codes (逗号分隔) | |||
| pickOrderConsoCode: string; | |||
| pickOrderStatus: string; | |||
| deliveryOrderId: number; | |||
| deliveryOrderIds: number[]; // ✅ 新增:所有 delivery order IDs | |||
| deliveryOrderIds: number[]; // 新增:所有 delivery order IDs | |||
| deliveryNo: string; | |||
| deliveryNos: string; // ✅ 新增:所有 delivery order codes (逗号分隔) | |||
| deliveryNos: string; // 新增:所有 delivery order codes (逗号分隔) | |||
| deliveryDate: string; | |||
| shopId: number; | |||
| shopCode: string; | |||
| @@ -352,13 +352,13 @@ export interface CompletedDoPickOrderResponse { | |||
| shopPoNo: string; | |||
| numberOfCartons: number; | |||
| truckLanceCode: string; | |||
| DepartureTime: string; // ✅ 新增 | |||
| DepartureTime: string; // 新增 | |||
| storeId: string; | |||
| completedDate: string; | |||
| fgPickOrders: FGPickOrderResponse[]; | |||
| } | |||
| // ✅ 新增:搜索参数接口 | |||
| // 新增:搜索参数接口 | |||
| export interface CompletedDoPickOrderSearchParams { | |||
| pickOrderCode?: string; | |||
| shopName?: string; | |||
| @@ -491,7 +491,8 @@ export async function assignByLane( | |||
| userId: number, | |||
| storeId: string, | |||
| truckLanceCode: string, | |||
| truckDepartureTime?: string | |||
| truckDepartureTime?: string, | |||
| requiredDate?: string | |||
| ): Promise<any> { | |||
| const response = await serverFetchJson( | |||
| `${BASE_API_URL}/doPickOrder/assign-by-lane`, | |||
| @@ -505,12 +506,13 @@ export async function assignByLane( | |||
| storeId, | |||
| truckLanceCode, | |||
| truckDepartureTime, | |||
| requiredDate, | |||
| }), | |||
| } | |||
| ); | |||
| return response; | |||
| } | |||
| // ✅ 新增:获取已完成的 DO Pick Orders API | |||
| // 新增:获取已完成的 DO Pick Orders API | |||
| export const fetchCompletedDoPickOrders = async ( | |||
| userId: number, | |||
| searchParams?: CompletedDoPickOrderSearchParams | |||
| @@ -919,7 +921,7 @@ export const fetchAllPickOrderLotsHierarchical = cache(async (userId: number): P | |||
| } | |||
| ); | |||
| console.log("✅ Fetched hierarchical lot details:", data); | |||
| console.log(" Fetched hierarchical lot details:", data); | |||
| return data; | |||
| } catch (error) { | |||
| console.error("❌ Error fetching hierarchical lot details:", error); | |||
| @@ -947,7 +949,7 @@ export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId: | |||
| } | |||
| ); | |||
| console.log("✅ Fetched hierarchical lot details:", data); | |||
| console.log(" Fetched hierarchical lot details:", data); | |||
| return data; | |||
| } catch (error) { | |||
| console.error("❌ Error fetching lot details:", error); | |||
| @@ -962,7 +964,7 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Pro | |||
| try { | |||
| console.log("🔍 Fetching all pick order line lot details for userId:", userId); | |||
| // ✅ Use the non-auto-assign endpoint | |||
| // Use the non-auto-assign endpoint | |||
| const data = await serverFetchJson<any[]>( | |||
| `${BASE_API_URL}/pickOrder/all-lots-with-details-no-auto-assign/${userId}`, | |||
| { | |||
| @@ -971,7 +973,7 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Pro | |||
| } | |||
| ); | |||
| console.log("✅ Fetched lot details:", data); | |||
| console.log(" Fetched lot details:", data); | |||
| return data; | |||
| } catch (error) { | |||
| console.error("❌ Error fetching lot details:", error); | |||
| @@ -987,7 +989,7 @@ export const fetchAllPickOrderDetails = cache(async (userId?: number) => { | |||
| }; | |||
| } | |||
| // ✅ Use the correct endpoint with userId in the path | |||
| // Use the correct endpoint with userId in the path | |||
| const url = `${BASE_API_URL}/pickOrder/detail-optimized/${userId}`; | |||
| return serverFetchJson<GetPickOrderInfoResponse>( | |||
| @@ -13,7 +13,7 @@ import { releaseDo, assignPickOrderByStore, releaseAssignedPickOrderByStore } fr | |||
| import DoInfoCard from "./DoInfoCard"; | |||
| import DoLineTable from "./DoLineTable"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; // ✅ Import the correct session type | |||
| import { SessionWithTokens } from "@/config/authConfig"; // Import the correct session type | |||
| type Props = { | |||
| id?: number; | |||
| @@ -30,9 +30,9 @@ const DoDetail: React.FC<Props> = ({ | |||
| const [serverError, setServerError] = useState(""); | |||
| const [successMessage, setSuccessMessage] = useState(""); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Use correct session type | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; // ✅ Get user ID from session.id | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; // Get user ID from session.id | |||
| console.log("🔍 DoSearch - session:", session); | |||
| console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| const formProps = useForm<DoDetailType>({ | |||
| @@ -50,7 +50,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| setSuccessMessage("") | |||
| if (id) { | |||
| // ✅ Get current user ID from session | |||
| // Get current user ID from session | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| if (!currentUserId) { | |||
| @@ -60,7 +60,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| const response = await releaseDo({ | |||
| id: id, | |||
| userId: currentUserId // ✅ Pass user ID from session | |||
| userId: currentUserId // Pass user ID from session | |||
| }) | |||
| if (response) { | |||
| @@ -74,16 +74,16 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| } finally { | |||
| setIsUploading(false) | |||
| } | |||
| }, [id, formProps, t, setIsUploading, session]) // ✅ Add session to dependencies | |||
| }, [id, formProps, t, setIsUploading, session]) // Add session to dependencies | |||
| // ✅ UPDATE STORE-BASED ASSIGNMENT HANDLERS | |||
| // UPDATE STORE-BASED ASSIGNMENT HANDLERS | |||
| const handleAssignByStore = useCallback(async (storeId: string) => { | |||
| try { | |||
| setIsAssigning(true) | |||
| setServerError("") | |||
| setSuccessMessage("") | |||
| // ✅ Get current user ID from session | |||
| // Get current user ID from session | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| if (!currentUserId) { | |||
| @@ -107,7 +107,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| } finally { | |||
| setIsAssigning(false) | |||
| } | |||
| }, [t, session]) // ✅ Add session to dependencies | |||
| }, [t, session]) // Add session to dependencies | |||
| const handleReleaseByStore = useCallback(async (storeId: string) => { | |||
| try { | |||
| @@ -115,7 +115,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| setServerError("") | |||
| setSuccessMessage("") | |||
| // ✅ Get current user ID from session | |||
| // Get current user ID from session | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| if (!currentUserId) { | |||
| @@ -139,7 +139,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| } finally { | |||
| setIsAssigning(false) | |||
| } | |||
| }, [t, session]) // ✅ Add session to dependencies | |||
| }, [t, session]) // Add session to dependencies | |||
| const onSubmit = useCallback<SubmitHandler<DoDetailType>>(async (data, event) => { | |||
| console.log(data) | |||
| @@ -182,7 +182,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| </Stack> | |||
| )} | |||
| {/* ✅ ADD STORE-BASED ASSIGNMENT BUTTONS */} | |||
| {/* ADD STORE-BASED ASSIGNMENT BUTTONS */} | |||
| { | |||
| formProps.watch("status")?.toLowerCase() === "released" && ( | |||
| <Box sx={{ mb: 2 }}> | |||
| @@ -34,7 +34,7 @@ interface CombinedLotTableProps { | |||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||
| } | |||
| // ✅ Simple helper function to check if item is completed | |||
| // Simple helper function to check if item is completed | |||
| const isItemCompleted = (lot: any) => { | |||
| const actualPickQty = Number(lot.actualPickQty) || 0; | |||
| const requiredQty = Number(lot.requiredQty) || 0; | |||
| @@ -60,7 +60,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| // ✅ Paginated data | |||
| // Paginated data | |||
| const paginatedLotData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| @@ -113,7 +113,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| const isCompleted = isItemCompleted(lot); | |||
| const isRejected = isItemRejected(lot); | |||
| // ✅ Green text color for completed items | |||
| // Green text color for completed items | |||
| const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit'; | |||
| return ( | |||
| @@ -47,7 +47,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string | |||
| truckLanceCode: string, | |||
| requiredDate: string | |||
| ) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| @@ -56,10 +57,10 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| setIsAssigning(true); | |||
| try { | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime); | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, requiredDate); | |||
| if (res.code === "SUCCESS") { | |||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | |||
| console.log(" Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadSummaries(); // 刷新按钮状态 | |||
| onPickOrderAssigned?.(); | |||
| @@ -231,7 +232,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -344,7 +345,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -27,22 +27,22 @@ const FGPickOrderInfoCard: React.FC<Props> = ({ fgOrder, doPickOrderDetail }) => | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| value={pickOrderCodes || ""} // ✅ 显示所有 pick order codes | |||
| label={t("Pick Order Code(s)")} // ✅ 修改标签 | |||
| value={pickOrderCodes || ""} // 显示所有 pick order codes | |||
| label={t("Pick Order Code(s)")} // 修改标签 | |||
| fullWidth | |||
| disabled={true} | |||
| multiline={pickOrderCodes.includes(',')} // ✅ 如果有多个代码,使用多行 | |||
| multiline={pickOrderCodes.includes(',')} // 如果有多个代码,使用多行 | |||
| rows={pickOrderCodes.includes(',') ? 2 : 1} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| value={deliveryOrderCodes || ""} // ✅ 显示所有 delivery order codes | |||
| label={t("Delivery Order Code(s)")} // ✅ 修改标签 | |||
| value={deliveryOrderCodes || ""} // 显示所有 delivery order codes | |||
| label={t("Delivery Order Code(s)")} // 修改标签 | |||
| fullWidth | |||
| disabled={true} | |||
| multiline={deliveryOrderCodes.includes(',')} // ✅ 如果有多个代码,使用多行 | |||
| multiline={deliveryOrderCodes.includes(',')} // 如果有多个代码,使用多行 | |||
| rows={deliveryOrderCodes.includes(',') ? 2 : 1} | |||
| /> | |||
| </Grid> | |||
| @@ -56,22 +56,31 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| loadSummaries(); | |||
| }, [loadSummaries]); | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string | |||
| ) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| } | |||
| setIsAssigning(true); | |||
| try { | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime); | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string, | |||
| requiredDate: string | |||
| ) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| } | |||
| let dateParam: string | undefined; | |||
| if (requiredDate === "today") { | |||
| dateParam = dayjs().format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "tomorrow") { | |||
| dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "dayAfterTomorrow") { | |||
| dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||
| } | |||
| setIsAssigning(true); | |||
| try { | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam); | |||
| if (res.code === "SUCCESS") { | |||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | |||
| console.log(" Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadSummaries(); // 刷新按钮状态 | |||
| onPickOrderAssigned?.(); | |||
| @@ -236,7 +245,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -336,7 +345,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -247,7 +247,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||
| const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail; | |||
| // ✅ 修复:根据标签页和事件来源决定是否更新打印按钮状态 | |||
| // 修复:根据标签页和事件来源决定是否更新打印按钮状态 | |||
| if (eventTabIndex === undefined || eventTabIndex === tabIndex) { | |||
| setPrintButtonsEnabled(allLotsCompleted); | |||
| console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted); | |||
| @@ -259,9 +259,9 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| return () => { | |||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| }; | |||
| }, [tabIndex]); // ✅ 添加 tabIndex 依赖 | |||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||
| // ✅ 新增:处理标签页切换时的打印按钮状态重置 | |||
| // 新增:处理标签页切换时的打印按钮状态重置 | |||
| useEffect(() => { | |||
| // 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态 | |||
| if (tabIndex === 2) { | |||
| @@ -286,7 +286,7 @@ const handleAssignByLane = useCallback(async ( | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime); | |||
| if (res.code === "SUCCESS") { | |||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | |||
| console.log(" Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadSummaries(); // 刷新按钮状态 | |||
| } else if (res.code === "USER_BUSY") { | |||
| @@ -322,7 +322,7 @@ const handleAssignByLane = useCallback(async ( | |||
| setIsAssigning(false); | |||
| } | |||
| }, [currentUserId, t, loadSummaries]); | |||
| // ✅ Manual assignment handler - uses the action function | |||
| // Manual assignment handler - uses the action function | |||
| */ | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| @@ -607,7 +607,7 @@ const handleAssignByLane = useCallback(async ( | |||
| </Grid> | |||
| </Box> | |||
| {/* Tabs section - ✅ Move the click handler here */} | |||
| {/* Tabs section - Move the click handler here */} | |||
| <Box sx={{ | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| @@ -26,7 +26,7 @@ import { | |||
| updateStockOutLineStatus, | |||
| createStockOutLine, | |||
| recordPickExecutionIssue, | |||
| fetchFGPickOrdersByUserId, // ✅ Add this import | |||
| fetchFGPickOrdersByUserId, // Add this import | |||
| FGPickOrderResponse, | |||
| autoAssignAndReleasePickOrder, | |||
| AutoAssignReleaseResponse, | |||
| @@ -59,13 +59,13 @@ interface Props { | |||
| onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void; | |||
| } | |||
| // ✅ QR Code Modal Component (from LotTable) | |||
| // QR Code Modal Component (from LotTable) | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| onClose: () => void; | |||
| lot: any | null; | |||
| onQrCodeSubmit: (lotNo: string) => void; | |||
| combinedLotData: any[]; // ✅ Add this prop | |||
| combinedLotData: any[]; // Add this prop | |||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| @@ -105,7 +105,7 @@ const QrCodeModal: React.FC<{ | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| if (stockInLineInfo.lotNo === lot.lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lot.lotNo}`); | |||
| setQrScanSuccess(true); | |||
| onQrCodeSubmit(lot.lotNo); | |||
| onClose(); | |||
| @@ -297,7 +297,7 @@ const QrCodeModal: React.FC<{ | |||
| {qrScanSuccess && ( | |||
| <Typography variant="caption" color="success" display="block"> | |||
| ✅ {t("Verified successfully!")} | |||
| {t("Verified successfully!")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| @@ -348,11 +348,11 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ Add QR modal states | |||
| // Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | |||
| // ✅ Add GoodPickExecutionForm states | |||
| // Add GoodPickExecutionForm states | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | |||
| const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); | |||
| @@ -389,14 +389,14 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [currentUserId, selectedPickOrderId]); | |||
| // ✅ 简化:移除复杂的 useEffect 依赖 | |||
| // 简化:移除复杂的 useEffect 依赖 | |||
| useEffect(() => { | |||
| if (currentUserId) { | |||
| fetchFgPickOrdersData(); | |||
| } | |||
| }, [currentUserId, fetchFgPickOrdersData]); | |||
| // ✅ Handle QR code button click | |||
| // Handle QR code button click | |||
| const handleQrCodeClick = (pickOrderId: number) => { | |||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | |||
| // TODO: Implement QR code functionality | |||
| @@ -424,22 +424,22 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| return; | |||
| } | |||
| // ✅ Use the non-auto-assign endpoint - this only fetches existing data | |||
| // Use the non-auto-assign endpoint - this only fetches existing data | |||
| const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse); | |||
| console.log("✅ All combined lot details:", allLotDetails); | |||
| console.log(" All combined lot details:", allLotDetails); | |||
| setCombinedLotData(allLotDetails); | |||
| setOriginalCombinedData(allLotDetails); | |||
| // ✅ 计算完成状态并发送事件 | |||
| // 计算完成状态并发送事件 | |||
| const allCompleted = allLotDetails.length > 0 && allLotDetails.every(lot => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| // ✅ 发送完成状态事件,包含标签页信息 | |||
| // 发送完成状态事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件 | |||
| tabIndex: 0 // 明确指定这是来自标签页 0 的事件 | |||
| } | |||
| })); | |||
| @@ -448,7 +448,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| // ✅ 如果加载失败,禁用打印按钮 | |||
| // 如果加载失败,禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -460,18 +460,18 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [currentUserId, combinedLotData]); | |||
| // ✅ Only fetch existing data when session is ready, no auto-assignment | |||
| // Only fetch existing data when session is ready, no auto-assignment | |||
| useEffect(() => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| console.log("✅ Session loaded, initializing pick order..."); | |||
| console.log(" Session loaded, initializing pick order..."); | |||
| initializationRef.current = true; | |||
| // ✅ Only fetch existing data, no auto-assignment | |||
| // Only fetch existing data, no auto-assignment | |||
| fetchAllCombinedLotData(); | |||
| } | |||
| }, [session, currentUserId, fetchAllCombinedLotData]); | |||
| // ✅ Add event listener for manual assignment | |||
| // Add event listener for manual assignment | |||
| useEffect(() => { | |||
| const handlePickOrderAssigned = () => { | |||
| console.log("🔄 Pick order assigned event received, refreshing data..."); | |||
| @@ -485,12 +485,12 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| }; | |||
| }, [fetchAllCombinedLotData]); | |||
| // ✅ Handle QR code submission for matched lot (external scanning) | |||
| // ✅ Handle QR code submission for matched lot (external scanning) | |||
| // Handle QR code submission for matched lot (external scanning) | |||
| // Handle QR code submission for matched lot (external scanning) | |||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||
| console.log(`✅ Processing QR Code for lot: ${lotNo}`); | |||
| console.log(` Processing QR Code for lot: ${lotNo}`); | |||
| // ✅ Use current data without refreshing to avoid infinite loop | |||
| // Use current data without refreshing to avoid infinite loop | |||
| const currentLotData = combinedLotData; | |||
| console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo)); | |||
| @@ -506,7 +506,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| return; | |||
| } | |||
| console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| console.log(` Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| setQrScanError(false); | |||
| try { | |||
| @@ -518,7 +518,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); | |||
| if (matchingLot.stockOutLineId) { | |||
| console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); | |||
| console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`); | |||
| existsCount++; | |||
| } else { | |||
| const stockOutLineData: CreateStockOutLine = { | |||
| @@ -533,10 +533,10 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result); | |||
| if (result && result.code === "EXISTS") { | |||
| console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); | |||
| console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`); | |||
| existsCount++; | |||
| } else if (result && result.code === "SUCCESS") { | |||
| console.log(`✅ Stock out line created successfully for line ${matchingLot.pickOrderLineId}`); | |||
| console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`); | |||
| successCount++; | |||
| } else { | |||
| console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result); | |||
| @@ -545,16 +545,16 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| } | |||
| // ✅ Always refresh data after processing (success or failure) | |||
| // Always refresh data after processing (success or failure) | |||
| console.log("🔄 Refreshing data after QR code processing..."); | |||
| await fetchAllCombinedLotData(); | |||
| if (successCount > 0 || existsCount > 0) { | |||
| console.log(`✅ QR Code processing completed: ${successCount} created, ${existsCount} already existed`); | |||
| console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`); | |||
| setQrScanSuccess(true); | |||
| setQrScanInput(''); // Clear input after successful processing | |||
| // ✅ Clear success state after a delay | |||
| // Clear success state after a delay | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| }, 2000); | |||
| @@ -563,7 +563,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| // ✅ Clear error state after a delay | |||
| // Clear error state after a delay | |||
| setTimeout(() => { | |||
| setQrScanError(false); | |||
| }, 3000); | |||
| @@ -573,10 +573,10 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| // ✅ Still refresh data even on error | |||
| // Still refresh data even on error | |||
| await fetchAllCombinedLotData(); | |||
| // ✅ Clear error state after a delay | |||
| // Clear error state after a delay | |||
| setTimeout(() => { | |||
| setQrScanError(false); | |||
| }, 3000); | |||
| @@ -589,17 +589,17 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [qrScanInput, handleQrCodeSubmit]); | |||
| // ✅ Handle QR code submission from modal (internal scanning) | |||
| // 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}`); | |||
| 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, // ✅ Use pickOrderConsoCode instead of pickOrderCode | |||
| consoCode: selectedLotForQr.pickOrderConsoCode, // Use pickOrderConsoCode instead of pickOrderCode | |||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | |||
| inventoryLotLineId: selectedLotForQr.lotId, | |||
| qty: 0.0 | |||
| @@ -620,7 +620,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| ...prev, | |||
| [lotKey]: requiredQty | |||
| })); | |||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| }, 500); | |||
| // Refresh data | |||
| @@ -631,7 +631,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [selectedLotForQr, fetchAllCombinedLotData]); | |||
| // ✅ Outside QR scanning - process QR codes from outside the page automatically | |||
| // Outside QR scanning - process QR codes from outside the page automatically | |||
| useEffect(() => { | |||
| if (qrValues.length > 0 && combinedLotData.length > 0) { | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| @@ -707,7 +707,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { | |||
| console.log("Found completed pick orders, auto-assigning next..."); | |||
| // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // await handleAutoAssignAndRelease(); // 删除这个函数 | |||
| } | |||
| } catch (error) { | |||
| @@ -715,7 +715,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ Handle submit pick quantity | |||
| // Handle submit pick quantity | |||
| const handleSubmitPickQty = useCallback(async (lot: any) => { | |||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||
| const newQty = pickQtyData[lotKey] || 0; | |||
| @@ -759,14 +759,14 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| }); | |||
| } | |||
| // ✅ FIXED: Use the proper API function instead of direct fetch | |||
| // 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...`); | |||
| console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); | |||
| try { | |||
| // ✅ Use the imported API function instead of direct fetch | |||
| // Use the imported API function instead of direct fetch | |||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); | |||
| console.log(`✅ Pick order completion check result:`, completionResponse); | |||
| console.log(` Pick order completion check result:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); | |||
| @@ -792,7 +792,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||
| // ✅ Handle reject lot | |||
| // Handle reject lot | |||
| const handleRejectLot = useCallback(async (lot: any) => { | |||
| if (!lot.stockOutLineId) { | |||
| console.error("No stock out line found for this lot"); | |||
| @@ -818,7 +818,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||
| // ✅ Handle pick execution form | |||
| // Handle pick execution form | |||
| const handlePickExecutionForm = useCallback((lot: any) => { | |||
| console.log("=== Pick Execution Form ==="); | |||
| console.log("Lot data:", lot); | |||
| @@ -847,7 +847,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| console.log("Pick execution issue recorded:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ Pick execution issue recorded successfully"); | |||
| console.log(" Pick execution issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| @@ -861,7 +861,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| } | |||
| }, [fetchAllCombinedLotData]); | |||
| // ✅ Calculate remaining required quantity | |||
| // Calculate remaining required quantity | |||
| const calculateRemainingRequiredQty = useCallback((lot: any) => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||
| @@ -942,7 +942,7 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| // Pagination data with sorting by routerIndex | |||
| const paginatedData = useMemo(() => { | |||
| // ✅ Sort by routerIndex first, then by other criteria | |||
| // Sort by routerIndex first, then by other criteria | |||
| const sortedData = [...combinedLotData].sort((a, b) => { | |||
| const aIndex = a.routerIndex || 0; | |||
| const bIndex = b.routerIndex || 0; | |||
| @@ -970,14 +970,14 @@ const fetchFgPickOrdersData = useCallback(async () => { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| {/* ✅ 修复:改进条件渲染逻辑 */} | |||
| {/* 修复:改进条件渲染逻辑 */} | |||
| {combinedDataLoading || fgPickOrdersLoading ? ( | |||
| // ✅ 数据加载中,显示加载指示器 | |||
| // 数据加载中,显示加载指示器 | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : fgPickOrders.length === 0 ? ( | |||
| // ✅ 没有活动订单,显示楼层选择面板 | |||
| // 没有活动订单,显示楼层选择面板 | |||
| <FinishedGoodFloorLanePanel | |||
| onPickOrderAssigned={() => { | |||
| if (currentUserId) { | |||
| @@ -987,7 +987,7 @@ return ( | |||
| }} | |||
| /> | |||
| ) : ( | |||
| // ✅ 有活动订单,显示 FG 订单信息 | |||
| // 有活动订单,显示 FG 订单信息 | |||
| <Box> | |||
| {fgPickOrders.map((fgOrder) => ( | |||
| <Box key={fgOrder.pickOrderId} sx={{ mb: 2 }}> | |||
| @@ -53,7 +53,7 @@ interface PickExecutionFormProps { | |||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||
| pickOrderId?: number; | |||
| pickOrderCreateDate: any; | |||
| // ✅ Remove these props since we're not handling normal cases | |||
| // Remove these props since we're not handling normal cases | |||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||
| // selectedRowId?: number | null; | |||
| } | |||
| @@ -75,7 +75,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| selectedPickOrderLine, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| // ✅ Remove these props | |||
| // Remove these props | |||
| // onNormalPickSubmit, | |||
| // selectedRowId, | |||
| }) => { | |||
| @@ -86,11 +86,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | |||
| // 计算剩余可用数量 | |||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | |||
| // ✅ 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty | |||
| // 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty | |||
| return lot.availableQty || 0; | |||
| }, []); | |||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| // ✅ Use the original required quantity, not subtracting actualPickQty | |||
| // Use the original required quantity, not subtracting actualPickQty | |||
| // The actualPickQty in the form should be independent of the database value | |||
| return lot.requiredQty || 0; | |||
| }, []); | |||
| @@ -175,7 +175,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| } | |||
| }, [errors]); | |||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| // Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| const req = selectedLot?.requiredQty || 0; | |||
| @@ -200,10 +200,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| }; | |||
| const handleSubmit = async () => { | |||
| // ✅ 先验证表单 | |||
| // First validate the form | |||
| if (!validateForm()) { | |||
| console.error('Form validation failed:', errors); | |||
| return; // ✅ 阻止提交,显示验证错误 | |||
| return; // Prevent submission, show validation errors | |||
| } | |||
| if (!formData.pickOrderId) { | |||
| @@ -214,10 +214,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| setLoading(true); | |||
| try { | |||
| await onSubmit(formData as PickExecutionIssueData); | |||
| // ✅ 成功时会自动关闭(由 onClose 处理) | |||
| // Automatically closed when successful (handled by onClose) | |||
| } catch (error: any) { | |||
| console.error('Error submitting pick execution issue:', error); | |||
| // ✅ 显示错误消息(可以通过 props 或 state 传递错误消息到父组件) | |||
| // Show error message (can be passed to parent component via props or state) | |||
| // 或者在这里显示 toast/alert | |||
| alert(t('Failed to submit issue. Please try again.') + (error.message ? `: ${error.message}` : '')); | |||
| } finally { | |||
| @@ -241,11 +241,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| return ( | |||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||
| <DialogTitle> | |||
| {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} | |||
| {t('Pick Execution Issue Form')} {/* Always show issue form title */} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mt: 2 }}> | |||
| {/* ✅ Add instruction text */} | |||
| {/* Add instruction text */} | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}> | |||
| @@ -255,7 +255,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| </Box> | |||
| </Grid> | |||
| {/* ✅ Keep the existing form fields */} | |||
| {/* Keep the existing form fields */} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| @@ -317,7 +317,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {/* Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| @@ -72,7 +72,7 @@ interface Props { | |||
| } | |||
| // ✅ 新增:Pick Order 数据接口 | |||
| // 新增:Pick Order 数据接口 | |||
| interface PickOrderData { | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| @@ -89,20 +89,20 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 新增:已完成 DO Pick Orders 状态 | |||
| // 新增:已完成 DO Pick Orders 状态 | |||
| const [completedDoPickOrders, setCompletedDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]); | |||
| const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false); | |||
| // ✅ 新增:详情视图状态 | |||
| // 新增:详情视图状态 | |||
| const [selectedDoPickOrder, setSelectedDoPickOrder] = useState<CompletedDoPickOrderResponse | null>(null); | |||
| const [showDetailView, setShowDetailView] = useState(false); | |||
| const [detailLotData, setDetailLotData] = useState<any[]>([]); | |||
| // ✅ 新增:搜索状态 | |||
| // 新增:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]); | |||
| // ✅ 新增:分页状态 | |||
| // 新增:分页状态 | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| @@ -307,7 +307,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [t]); | |||
| // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders | |||
| // 修改:使用新的 API 获取已完成的 DO Pick Orders | |||
| const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => { | |||
| if (!currentUserId) return; | |||
| @@ -319,7 +319,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| setCompletedDoPickOrders(completedDoPickOrders); | |||
| setFilteredDoPickOrders(completedDoPickOrders); | |||
| console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders); | |||
| console.log(" Fetched completed DO pick orders:", completedDoPickOrders); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching completed DO pick orders:", error); | |||
| setCompletedDoPickOrders([]); | |||
| @@ -329,14 +329,14 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 初始化时获取数据 | |||
| // 初始化时获取数据 | |||
| useEffect(() => { | |||
| if (currentUserId) { | |||
| fetchCompletedDoPickOrdersData(); | |||
| } | |||
| }, [currentUserId, fetchCompletedDoPickOrdersData]); | |||
| // ✅ 修改:搜索功能使用新的 API | |||
| // 修改:搜索功能使用新的 API | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| @@ -352,13 +352,13 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| fetchCompletedDoPickOrdersData(searchParams); | |||
| }, [fetchCompletedDoPickOrdersData]); | |||
| // ✅ 修复:重命名函数避免重复声明 | |||
| // 修复:重命名函数避免重复声明 | |||
| const handleSearchReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| fetchCompletedDoPickOrdersData(); // 重新获取所有数据 | |||
| }, [fetchCompletedDoPickOrdersData]); | |||
| // ✅ 分页功能 | |||
| // 分页功能 | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| ...prev, | |||
| @@ -374,14 +374,14 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| }, []); | |||
| // ✅ 分页数据 | |||
| // 分页数据 | |||
| const paginatedData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| return filteredDoPickOrders.slice(startIndex, endIndex); | |||
| }, [filteredDoPickOrders, paginationController]); | |||
| // ✅ 搜索条件 | |||
| // 搜索条件 | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| @@ -405,11 +405,11 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| setShowDetailView(true); | |||
| try { | |||
| // ✅ 使用 doPickOrderRecordId 而不是 pickOrderId | |||
| // 使用 doPickOrderRecordId 而不是 pickOrderId | |||
| const hierarchicalData = await fetchLotDetailsByDoPickOrderRecordId(doPickOrder.doPickOrderRecordId); | |||
| console.log("✅ Loaded hierarchical lot data:", hierarchicalData); | |||
| console.log(" Loaded hierarchical lot data:", hierarchicalData); | |||
| // ✅ 转换为平铺格式 | |||
| // 转换为平铺格式 | |||
| const flatLotData: any[] = []; | |||
| if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) { | |||
| @@ -465,7 +465,7 @@ if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) { | |||
| } | |||
| }); | |||
| } else if (lineStockouts.length > 0) { | |||
| // ✅ lots 为空但有 stockouts(如「雞絲碗仔翅」),也要显示 | |||
| // lots 为空但有 stockouts(如「雞絲碗仔翅」),也要显示 | |||
| lineStockouts.forEach((so: any) => { | |||
| flatLotData.push({ | |||
| pickOrderCode: po.pickOrderCode, | |||
| @@ -488,7 +488,7 @@ if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) { | |||
| setDetailLotData(flatLotData); | |||
| // ✅ 计算完成状态 | |||
| // 计算完成状态 | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every(lot => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| @@ -500,7 +500,7 @@ setDetailLotData(flatLotData); | |||
| } | |||
| })); | |||
| } catch (error) { // ✅ 添加 catch 块 | |||
| } catch (error) { // 添加 catch 块 | |||
| console.error("❌ Error loading detail lot data:", error); | |||
| setDetailLotData([]); | |||
| @@ -514,13 +514,13 @@ setDetailLotData(flatLotData); | |||
| }, []); | |||
| // ✅ 返回列表视图 | |||
| // 返回列表视图 | |||
| const handleBackToList = useCallback(() => { | |||
| setShowDetailView(false); | |||
| setSelectedDoPickOrder(null); | |||
| setDetailLotData([]); | |||
| // ✅ 返回列表时禁用打印按钮 | |||
| // 返回列表时禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -530,8 +530,8 @@ setDetailLotData(flatLotData); | |||
| }, []); | |||
| // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格 | |||
| // ✅ 如果显示详情视图,渲染层级结构 | |||
| // 如果显示详情视图,渲染类似 GoodPickExecution 的表格 | |||
| // 如果显示详情视图,渲染层级结构 | |||
| if (showDetailView && selectedDoPickOrder) { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| @@ -567,7 +567,7 @@ if (showDetailView && selectedDoPickOrder) { | |||
| </Stack> | |||
| </Paper> | |||
| {/* ✅ 添加:多个 Pick Orders 信息(如果有) */} | |||
| {/* 添加:多个 Pick Orders 信息(如果有) */} | |||
| {selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && ( | |||
| <Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}> | |||
| <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}> | |||
| @@ -586,7 +586,7 @@ if (showDetailView && selectedDoPickOrder) { | |||
| </Paper> | |||
| )} | |||
| {/* ✅ 数据检查 */} | |||
| {/* 数据检查 */} | |||
| {detailLotData.length === 0 ? ( | |||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| @@ -594,16 +594,16 @@ if (showDetailView && selectedDoPickOrder) { | |||
| </Typography> | |||
| </Box> | |||
| ) : ( | |||
| /* ✅ 按 Pick Order 分组显示 */ | |||
| /* 按 Pick Order 分组显示 */ | |||
| <Stack spacing={2}> | |||
| {/* ✅ 按 pickOrderCode 分组 */} | |||
| {/* 按 pickOrderCode 分组 */} | |||
| {Object.entries( | |||
| detailLotData.reduce((acc: any, lot: any) => { | |||
| const key = lot.pickOrderCode || 'Unknown'; | |||
| if (!acc[key]) { | |||
| acc[key] = { | |||
| lots: [], | |||
| deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // ✅ 保存对应的 deliveryOrderCode | |||
| deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // 保存对应的 deliveryOrderCode | |||
| }; | |||
| } | |||
| acc[key].lots.push(lot); | |||
| @@ -615,7 +615,7 @@ if (showDetailView && selectedDoPickOrder) { | |||
| <Typography variant="subtitle1" fontWeight="bold"> | |||
| {t("Pick Order")}: {pickOrderCode} ({data.lots.length} {t("items")}) | |||
| {" | "} | |||
| {t("Delivery Order")}: {data.deliveryOrderCode} {/* ✅ 使用保存的 deliveryOrderCode */} | |||
| {t("Delivery Order")}: {data.deliveryOrderCode} {/* 使用保存的 deliveryOrderCode */} | |||
| </Typography> | |||
| </AccordionSummary> | |||
| <AccordionDetails> | |||
| @@ -665,7 +665,7 @@ if (showDetailView && selectedDoPickOrder) { | |||
| ); | |||
| } | |||
| // ✅ 默认列表视图 | |||
| // 默认列表视图 | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Box> | |||
| @@ -32,7 +32,7 @@ import { | |||
| createStockOutLine, | |||
| updateStockOutLine, | |||
| recordPickExecutionIssue, | |||
| fetchFGPickOrders, // ✅ Add this import | |||
| fetchFGPickOrders, // Add this import | |||
| FGPickOrderResponse, | |||
| stockReponse, | |||
| PickExecutionIssueData, | |||
| @@ -42,8 +42,8 @@ import { | |||
| checkAndCompletePickOrderByConsoCode, | |||
| updateSuggestedLotLineId, | |||
| confirmLotSubstitution, | |||
| fetchDoPickOrderDetail, // ✅ 必须添加 | |||
| DoPickOrderDetail, // ✅ 必须添加 | |||
| fetchDoPickOrderDetail, // 必须添加 | |||
| DoPickOrderDetail, // 必须添加 | |||
| fetchFGPickOrdersByUserId | |||
| } from "@/app/api/pickOrder/actions"; | |||
| @@ -70,13 +70,13 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ QR Code Modal Component (from LotTable) | |||
| // QR Code Modal Component (from LotTable) | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| onClose: () => void; | |||
| lot: any | null; | |||
| onQrCodeSubmit: (lotNo: string) => void; | |||
| combinedLotData: any[]; // ✅ Add this prop | |||
| combinedLotData: any[]; // Add this prop | |||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| @@ -116,7 +116,7 @@ const QrCodeModal: React.FC<{ | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| if (stockInLineInfo.lotNo === lot.lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lot.lotNo}`); | |||
| setQrScanSuccess(true); | |||
| onQrCodeSubmit(lot.lotNo); | |||
| onClose(); | |||
| @@ -308,7 +308,7 @@ const QrCodeModal: React.FC<{ | |||
| {qrScanSuccess && ( | |||
| <Typography variant="caption" color="success" display="block"> | |||
| ✅ {t("Verified successfully!")} | |||
| {t("Verified successfully!")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| @@ -359,20 +359,20 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ Add QR modal states | |||
| // Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | |||
| const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false); | |||
| const [expectedLotData, setExpectedLotData] = useState<any>(null); | |||
| const [scannedLotData, setScannedLotData] = useState<any>(null); | |||
| const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| // ✅ Add GoodPickExecutionForm states | |||
| // Add GoodPickExecutionForm states | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | |||
| const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); | |||
| const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); | |||
| // ✅ Add these missing state variables after line 352 | |||
| // Add these missing state variables after line 352 | |||
| const [isManualScanning, setIsManualScanning] = useState<boolean>(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| @@ -381,7 +381,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| // ✅ Handle QR code button click | |||
| // Handle QR code button click | |||
| const handleQrCodeClick = (pickOrderId: number) => { | |||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | |||
| // TODO: Implement QR code functionality | |||
| @@ -435,11 +435,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| return; | |||
| } | |||
| // ✅ 获取新结构的层级数据 | |||
| // 获取新结构的层级数据 | |||
| const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse); | |||
| console.log("✅ Hierarchical data (new structure):", hierarchicalData); | |||
| console.log(" Hierarchical data (new structure):", hierarchicalData); | |||
| // ✅ 检查数据结构 | |||
| // 检查数据结构 | |||
| if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) { | |||
| console.warn("⚠️ No FG info or pick orders found"); | |||
| setCombinedLotData([]); | |||
| @@ -448,10 +448,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| return; | |||
| } | |||
| // ✅ 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据) | |||
| // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据) | |||
| const mergedPickOrder = hierarchicalData.pickOrders[0]; | |||
| // ✅ 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片) | |||
| // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片) | |||
| // 修改第 478-509 行的 fgOrder 构建逻辑: | |||
| const fgOrder: FGPickOrderResponse = { | |||
| @@ -464,7 +464,7 @@ const fgOrder: FGPickOrderResponse = { | |||
| DepartureTime: hierarchicalData.fgInfo.departureTime, | |||
| shopAddress: "", | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| // ✅ 兼容字段 | |||
| // 兼容字段 | |||
| pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, | |||
| pickOrderConsoCode: mergedPickOrder.consoCode || "", | |||
| pickOrderTargetDate: mergedPickOrder.targetDate || "", | |||
| @@ -477,16 +477,16 @@ const fgOrder: FGPickOrderResponse = { | |||
| numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0, | |||
| qrCodeData: hierarchicalData.fgInfo.doPickOrderId, | |||
| // ✅ 新增:多个 pick orders 信息 - 保持数组格式,不要 join | |||
| // 新增:多个 pick orders 信息 - 保持数组格式,不要 join | |||
| numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0, | |||
| pickOrderIds: mergedPickOrder.pickOrderIds || [], | |||
| pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes) | |||
| ? mergedPickOrder.pickOrderCodes | |||
| : [], // ✅ 改:保持数组 | |||
| : [], // 改:保持数组 | |||
| deliveryOrderIds: mergedPickOrder.doOrderIds || [], | |||
| deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes) | |||
| ? mergedPickOrder.deliveryOrderCodes | |||
| : [], // ✅ 改:保持数组 | |||
| : [], // 改:保持数组 | |||
| lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder) | |||
| ? mergedPickOrder.lineCountsPerPickOrder | |||
| : [] | |||
| @@ -499,38 +499,38 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| // ❌ 移除:不需要 doPickOrderDetail 和 switcher 逻辑 | |||
| // if (hierarchicalData.pickOrders.length > 1) { ... } | |||
| // ✅ 直接使用合并后的 pickOrderLines | |||
| // 直接使用合并后的 pickOrderLines | |||
| console.log("🎯 Displaying merged pick order lines"); | |||
| // ✅ 将层级数据转换为平铺格式(用于表格显示) | |||
| // 将层级数据转换为平铺格式(用于表格显示) | |||
| const flatLotData: any[] = []; | |||
| mergedPickOrder.pickOrderLines.forEach((line: any) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| // ✅ 修复:先对 lots 按 lotId 去重并合并 requiredQty | |||
| // 修复:先对 lots 按 lotId 去重并合并 requiredQty | |||
| const lotMap = new Map<number, any>(); | |||
| line.lots.forEach((lot: any) => { | |||
| const lotId = lot.id; | |||
| if (lotMap.has(lotId)) { | |||
| // ✅ 如果已存在,合并 requiredQty | |||
| // 如果已存在,合并 requiredQty | |||
| const existingLot = lotMap.get(lotId); | |||
| existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0); | |||
| // ✅ 保留其他字段(使用第一个遇到的 lot 的字段) | |||
| // 保留其他字段(使用第一个遇到的 lot 的字段) | |||
| } else { | |||
| // ✅ 首次遇到,添加到 map | |||
| // 首次遇到,添加到 map | |||
| lotMap.set(lotId, { ...lot }); | |||
| } | |||
| }); | |||
| // ✅ 遍历去重后的 lots | |||
| // 遍历去重后的 lots | |||
| lotMap.forEach((lot: any) => { | |||
| flatLotData.push({ | |||
| // ✅ 使用合并后的数据 | |||
| // 使用合并后的数据 | |||
| pickOrderConsoCode: mergedPickOrder.consoCode, | |||
| pickOrderTargetDate: mergedPickOrder.targetDate, | |||
| pickOrderStatus: mergedPickOrder.status, | |||
| pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // ✅ 使用第一个 pickOrderId | |||
| pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| @@ -548,7 +548,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| location: lot.location, | |||
| stockUnit: lot.stockUnit, | |||
| availableQty: lot.availableQty, | |||
| requiredQty: lot.requiredQty, // ✅ 使用合并后的 requiredQty | |||
| requiredQty: lot.requiredQty, // 使用合并后的 requiredQty | |||
| actualPickQty: lot.actualPickQty, | |||
| inQty: lot.inQty, | |||
| outQty: lot.outQty, | |||
| @@ -569,16 +569,16 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| }); | |||
| }); | |||
| } else { | |||
| // ✅ 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id | |||
| // 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id | |||
| const firstStockout = line.stockouts && line.stockouts.length > 0 | |||
| ? line.stockouts[0] | |||
| : null; | |||
| flatLotData.push({ | |||
| pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // ✅ 修复:consoCodes 是数组 | |||
| pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // 修复:consoCodes 是数组 | |||
| pickOrderTargetDate: mergedPickOrder.targetDate, | |||
| pickOrderStatus: mergedPickOrder.status, | |||
| pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // ✅ 使用第一个 pickOrderId | |||
| pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| @@ -590,7 +590,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| uomDesc: line.item.uomDesc, | |||
| uomShortDesc: line.item.uomShortDesc, | |||
| // ✅ Null stock 字段 - 从 stockouts 数组中获取 | |||
| // Null stock 字段 - 从 stockouts 数组中获取 | |||
| lotId: firstStockout?.lotId || null, | |||
| lotNo: firstStockout?.lotNo || null, | |||
| expiryDate: null, | |||
| @@ -606,7 +606,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| lotAvailability: 'insufficient_stock', | |||
| processingStatus: firstStockout?.status || 'pending', | |||
| suggestedPickLotId: null, | |||
| stockOutLineId: firstStockout?.id || null, // ✅ 使用 stockouts 数组中的 id | |||
| stockOutLineId: firstStockout?.id || null, // 使用 stockouts 数组中的 id | |||
| stockOutLineStatus: firstStockout?.status || null, | |||
| stockOutLineQty: firstStockout?.qty || 0, | |||
| @@ -619,7 +619,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| }); | |||
| console.log("✅ Transformed flat lot data:", flatLotData); | |||
| console.log(" Transformed flat lot data:", flatLotData); | |||
| console.log("🔍 Total items (including null stock):", flatLotData.length); | |||
| setCombinedLotData(flatLotData); | |||
| @@ -635,25 +635,25 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, [currentUserId, checkAllLotsCompleted]); // ❌ 移除 selectedPickOrderId 依赖 | |||
| // ✅ Add effect to check completion when lot data changes | |||
| // Add effect to check completion when lot data changes | |||
| useEffect(() => { | |||
| if (combinedLotData.length > 0) { | |||
| checkAllLotsCompleted(combinedLotData); | |||
| } | |||
| }, [combinedLotData, checkAllLotsCompleted]); | |||
| // ✅ Add function to expose completion status to parent | |||
| // Add function to expose completion status to parent | |||
| const getCompletionStatus = useCallback(() => { | |||
| return allLotsCompleted; | |||
| }, [allLotsCompleted]); | |||
| // ✅ Expose completion status to parent component | |||
| // Expose completion status to parent component | |||
| useEffect(() => { | |||
| // Dispatch custom event with completion status | |||
| const event = new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted, | |||
| tabIndex: 1 // ✅ 明确指定这是来自标签页 1 的事件 | |||
| tabIndex: 1 // 明确指定这是来自标签页 1 的事件 | |||
| } | |||
| }); | |||
| window.dispatchEvent(event); | |||
| @@ -708,22 +708,22 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]); | |||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||
| console.log(`✅ Processing QR Code for lot: ${lotNo}`); | |||
| console.log(` Processing QR Code for lot: ${lotNo}`); | |||
| // ✅ 检查 lotNo 是否为 null 或 undefined(包括字符串 "null") | |||
| // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null") | |||
| if (!lotNo || lotNo === 'null' || lotNo.trim() === '') { | |||
| console.error("❌ Invalid lotNo: null, undefined, or empty"); | |||
| return; | |||
| } | |||
| // ✅ Use current data without refreshing to avoid infinite loop | |||
| // Use current data without refreshing to avoid infinite loop | |||
| const currentLotData = combinedLotData; | |||
| console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo)); | |||
| // ✅ 修复:在比较前确保 lotNo 不为 null | |||
| // 修复:在比较前确保 lotNo 不为 null | |||
| const lotNoLower = lotNo.toLowerCase(); | |||
| const matchingLots = currentLotData.filter(lot => { | |||
| if (!lot.lotNo) return false; // ✅ 跳过 null lotNo | |||
| if (!lot.lotNo) return false; // 跳过 null lotNo | |||
| return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower; | |||
| }); | |||
| @@ -736,7 +736,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return; | |||
| } | |||
| console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| console.log(` Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| setQrScanError(false); | |||
| try { | |||
| @@ -811,20 +811,20 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| } | |||
| // ✅ FIXED: Set refresh flag before refreshing data | |||
| // FIXED: Set refresh flag before refreshing data | |||
| setIsRefreshingData(true); | |||
| console.log("🔄 Refreshing data after QR code processing..."); | |||
| await fetchAllCombinedLotData(); | |||
| if (successCount > 0) { | |||
| console.log(`✅ QR Code processing completed: ${successCount} updated/created`); | |||
| console.log(` QR Code processing completed: ${successCount} updated/created`); | |||
| setQrScanSuccess(true); | |||
| setQrScanError(false); | |||
| setQrScanInput(''); // Clear input after successful processing | |||
| //setIsManualScanning(false); | |||
| // stopScan(); | |||
| // resetScan(); | |||
| // ✅ Clear success state after a delay | |||
| // Clear success state after a delay | |||
| //setTimeout(() => { | |||
| //setQrScanSuccess(false); | |||
| @@ -834,7 +834,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| // ✅ Clear error state after a delay | |||
| // Clear error state after a delay | |||
| // setTimeout(() => { | |||
| // setQrScanError(false); | |||
| //}, 3000); | |||
| @@ -844,16 +844,16 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| // ✅ Still refresh data even on error | |||
| // Still refresh data even on error | |||
| setIsRefreshingData(true); | |||
| await fetchAllCombinedLotData(); | |||
| // ✅ Clear error state after a delay | |||
| // Clear error state after a delay | |||
| setTimeout(() => { | |||
| setQrScanError(false); | |||
| }, 3000); | |||
| } finally { | |||
| // ✅ Clear refresh flag after a short delay | |||
| // Clear refresh flag after a short delay | |||
| setTimeout(() => { | |||
| setIsRefreshingData(false); | |||
| }, 1000); | |||
| @@ -914,7 +914,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return; | |||
| } | |||
| // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots) | |||
| // FIXED: Find the ACTIVE suggested lot (not rejected lots) | |||
| const activeSuggestedLots = sameItemLotsInExpected.filter(lot => | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' && | |||
| @@ -942,7 +942,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| // Case 2: Item matches but lot number differs -> open confirmation modal | |||
| // ✅ FIXED: Use the first ACTIVE suggested lot, not just any lot | |||
| // FIXED: Use the first ACTIVE suggested lot, not just any lot | |||
| const expectedLot = activeSuggestedLots[0]; | |||
| if (!expectedLot) { | |||
| console.error("Could not determine expected lot for confirmation"); | |||
| @@ -951,7 +951,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return; | |||
| } | |||
| // ✅ Check if the expected lot is already the scanned lot (after substitution) | |||
| // Check if the expected lot is already the scanned lot (after substitution) | |||
| if (expectedLot.lotNo === scanned?.lotNo) { | |||
| console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); | |||
| handleQrCodeSubmit(scanned.lotNo); | |||
| @@ -981,8 +981,8 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return; | |||
| } | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]); | |||
| // ✅ Update the outside QR scanning effect to use enhanced processing | |||
| // ✅ Update the outside QR scanning effect to use enhanced processing | |||
| // Update the outside QR scanning effect to use enhanced processing | |||
| // Update the outside QR scanning effect to use enhanced processing | |||
| useEffect(() => { | |||
| if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { | |||
| return; | |||
| @@ -1003,18 +1003,18 @@ useEffect(() => { | |||
| processOutsideQrCode(latestQr); | |||
| } | |||
| }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]); | |||
| // ✅ Only fetch existing data when session is ready, no auto-assignment | |||
| // Only fetch existing data when session is ready, no auto-assignment | |||
| useEffect(() => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| console.log("✅ Session loaded, initializing pick order..."); | |||
| console.log(" Session loaded, initializing pick order..."); | |||
| initializationRef.current = true; | |||
| // ✅ Only fetch existing data, no auto-assignment | |||
| // Only fetch existing data, no auto-assignment | |||
| fetchAllCombinedLotData(); | |||
| } | |||
| }, [session, currentUserId, fetchAllCombinedLotData]); | |||
| // ✅ Add event listener for manual assignment | |||
| // Add event listener for manual assignment | |||
| useEffect(() => { | |||
| const handlePickOrderAssigned = () => { | |||
| console.log("🔄 Pick order assigned event received, refreshing data..."); | |||
| @@ -1036,10 +1036,10 @@ useEffect(() => { | |||
| } | |||
| }, [qrScanInput, handleQrCodeSubmit]); | |||
| // ✅ Handle QR code submission from modal (internal scanning) | |||
| // 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}`); | |||
| console.log(` QR Code verified for lot: ${lotNo}`); | |||
| const requiredQty = selectedLotForQr.requiredQty; | |||
| const lotId = selectedLotForQr.lotId; | |||
| @@ -1068,7 +1068,7 @@ useEffect(() => { | |||
| ...prev, | |||
| [lotKey]: requiredQty | |||
| })); | |||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| }, 500); | |||
| // Refresh data | |||
| @@ -1117,7 +1117,7 @@ useEffect(() => { | |||
| if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { | |||
| console.log("Found completed pick orders, auto-assigning next..."); | |||
| // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // await handleAutoAssignAndRelease(); // 删除这个函数 | |||
| } | |||
| } catch (error) { | |||
| @@ -1125,7 +1125,7 @@ useEffect(() => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ Handle submit pick quantity | |||
| // Handle submit pick quantity | |||
| const handleSubmitPickQty = useCallback(async (lot: any) => { | |||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||
| const newQty = pickQtyData[lotKey] || 0; | |||
| @@ -1136,11 +1136,11 @@ useEffect(() => { | |||
| } | |||
| try { | |||
| // ✅ FIXED: Calculate cumulative quantity correctly | |||
| // FIXED: Calculate cumulative quantity correctly | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + newQty; | |||
| // ✅ FIXED: Determine status based on cumulative quantity vs required quantity | |||
| // FIXED: Determine status based on cumulative quantity vs required quantity | |||
| let newStatus = 'partially_completed'; | |||
| if (cumulativeQty >= lot.requiredQty) { | |||
| @@ -1163,7 +1163,7 @@ useEffect(() => { | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: newStatus, | |||
| qty: cumulativeQty // ✅ Use cumulative quantity | |||
| qty: cumulativeQty // Use cumulative quantity | |||
| }); | |||
| if (newQty > 0) { | |||
| @@ -1175,13 +1175,13 @@ useEffect(() => { | |||
| }); | |||
| } | |||
| // ✅ Check if pick order is completed when lot status becomes 'completed' | |||
| // 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...`); | |||
| 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); | |||
| console.log(` Pick order completion check result:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); | |||
| @@ -1207,7 +1207,7 @@ useEffect(() => { | |||
| } | |||
| }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||
| // ✅ Handle reject lot | |||
| // Handle reject lot | |||
| const handleRejectLot = useCallback(async (lot: any) => { | |||
| if (!lot.stockOutLineId) { | |||
| console.error("No stock out line found for this lot"); | |||
| @@ -1233,7 +1233,7 @@ useEffect(() => { | |||
| } | |||
| }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||
| // ✅ Handle pick execution form | |||
| // Handle pick execution form | |||
| const handlePickExecutionForm = useCallback((lot: any) => { | |||
| console.log("=== Pick Execution Form ==="); | |||
| console.log("Lot data:", lot); | |||
| @@ -1264,7 +1264,7 @@ useEffect(() => { | |||
| console.log("Pick execution issue recorded:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ Pick execution issue recorded successfully"); | |||
| console.log(" Pick execution issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| @@ -1285,7 +1285,7 @@ useEffect(() => { | |||
| } | |||
| }, [fetchAllCombinedLotData]); | |||
| // ✅ Calculate remaining required quantity | |||
| // Calculate remaining required quantity | |||
| const calculateRemainingRequiredQty = useCallback((lot: any) => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||
| @@ -1369,7 +1369,7 @@ useEffect(() => { | |||
| const paginatedData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| return combinedLotData.slice(startIndex, endIndex); // ✅ No sorting needed | |||
| return combinedLotData.slice(startIndex, endIndex); // No sorting needed | |||
| }, [combinedLotData, paginationController]); | |||
| const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { | |||
| if (!lot.stockOutLineId) { | |||
| @@ -1378,11 +1378,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| } | |||
| try { | |||
| // ✅ FIXED: Calculate cumulative quantity correctly | |||
| // FIXED: Calculate cumulative quantity correctly | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + submitQty; | |||
| // ✅ FIXED: Determine status based on cumulative quantity vs required quantity | |||
| // FIXED: Determine status based on cumulative quantity vs required quantity | |||
| let newStatus = 'partially_completed'; | |||
| if (cumulativeQty >= lot.requiredQty) { | |||
| @@ -1405,7 +1405,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: newStatus, | |||
| qty: cumulativeQty // ✅ Use cumulative quantity | |||
| qty: cumulativeQty // Use cumulative quantity | |||
| }); | |||
| if (submitQty > 0) { | |||
| @@ -1417,13 +1417,13 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| }); | |||
| } | |||
| // ✅ Check if pick order is completed when lot status becomes 'completed' | |||
| // 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...`); | |||
| 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); | |||
| console.log(` Pick order completion check result:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`); | |||
| @@ -1450,7 +1450,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| }, [fetchAllCombinedLotData, checkAndAutoAssignNext]); | |||
| // ✅ Add these functions after line 395 | |||
| // Add these functions after line 395 | |||
| const handleStartScan = useCallback(() => { | |||
| console.log(" Starting manual QR scan..."); | |||
| setIsManualScanning(true); | |||
| @@ -1468,7 +1468,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| console.log("🔍 Switching to pick order:", pickOrderId); | |||
| setSelectedPickOrderId(pickOrderId); | |||
| // ✅ 强制刷新数据,确保显示正确的 pick order 数据 | |||
| // 强制刷新数据,确保显示正确的 pick order 数据 | |||
| await fetchAllCombinedLotData(currentUserId, pickOrderId); | |||
| } catch (error) { | |||
| console.error("Error switching pick order:", error); | |||
| @@ -1487,7 +1487,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| }, [stopScan, resetScan]); | |||
| // ... existing code around line 1469 ... | |||
| const handlelotnull = useCallback(async (lot: any) => { | |||
| // ✅ 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId | |||
| // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId | |||
| const stockOutLineId = lot.stockOutLineId; | |||
| if (!stockOutLineId) { | |||
| @@ -1496,14 +1496,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| } | |||
| try { | |||
| // ✅ Step 1: Update stock out line status | |||
| // Step 1: Update stock out line status | |||
| await updateStockOutLineStatus({ | |||
| id: stockOutLineId, | |||
| status: 'completed', | |||
| qty: 0 | |||
| }); | |||
| // ✅ Step 2: Create pick execution issue for no-lot case | |||
| // Step 2: Create pick execution issue for no-lot case | |||
| // Get pick order ID from fgPickOrders or use 0 if not available | |||
| const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0; | |||
| const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || ''; | |||
| @@ -1512,18 +1512,18 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| type: "Do", // Delivery Order type | |||
| pickOrderId: pickOrderId, | |||
| pickOrderCode: pickOrderCode, | |||
| pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // ✅ Use dayjs format | |||
| pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format | |||
| pickExecutionDate: dayjs().format('YYYY-MM-DD'), | |||
| pickOrderLineId: lot.pickOrderLineId, | |||
| itemId: lot.itemId, | |||
| itemCode: lot.itemCode || '', | |||
| itemDescription: lot.itemName || '', | |||
| lotId: null, // ✅ No lot available | |||
| lotNo: null, // ✅ No lot number | |||
| lotId: null, // No lot available | |||
| lotNo: null, // No lot number | |||
| storeLocation: lot.location || '', | |||
| requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, | |||
| actualPickQty: 0, // ✅ No items picked (no lot available) | |||
| missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // ✅ All quantity is missing | |||
| actualPickQty: 0, // No items picked (no lot available) | |||
| missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing | |||
| badItemQty: 0, | |||
| issueRemark: `No lot available for this item. Handled via handlelotnull.`, | |||
| pickerName: session?.user?.name || '', | |||
| @@ -1531,15 +1531,15 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| }; | |||
| const result = await recordPickExecutionIssue(issueData); | |||
| console.log("✅ Pick execution issue created for no-lot item:", result); | |||
| console.log(" Pick execution issue created for no-lot item:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ No-lot item handled and issue recorded successfully"); | |||
| console.log(" No-lot item handled and issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| // ✅ Step 3: Refresh data | |||
| // Step 3: Refresh data | |||
| await fetchAllCombinedLotData(); | |||
| } catch (error) { | |||
| console.error("❌ Error in handlelotnull:", error); | |||
| @@ -1548,14 +1548,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| // ... existing code ... | |||
| const handleSubmitAllScanned = useCallback(async () => { | |||
| const scannedLots = combinedLotData.filter(lot => { | |||
| // ✅ 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete | |||
| // 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete | |||
| if (lot.noLot === true) { | |||
| return lot.stockOutLineStatus === 'checked' || | |||
| lot.stockOutLineStatus === 'pending' || | |||
| lot.stockOutLineStatus === 'partially_completed' || | |||
| lot.stockOutLineStatus === 'PARTIALLY_COMPLETE'; | |||
| } | |||
| // ✅ 正常情况:只包含 checked 状态 | |||
| // 正常情况:只包含 checked 状态 | |||
| return lot.stockOutLineStatus === 'checked'; | |||
| }); | |||
| @@ -1568,35 +1568,35 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||
| try { | |||
| // ✅ Submit all items in parallel using Promise.all | |||
| // Submit all items in parallel using Promise.all | |||
| const submitPromises = scannedLots.map(async (lot) => { | |||
| // ✅ 检查是否是 noLot 情况 | |||
| // 检查是否是 noLot 情况 | |||
| if (lot.noLot === true) { | |||
| // ✅ 使用 handlelotnull 处理无 lot 的情况 | |||
| // 使用 handlelotnull 处理无 lot 的情况 | |||
| console.log(`Submitting no-lot item: ${lot.itemName || lot.itemCode}`); | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: 'completed', | |||
| qty: 0 | |||
| }); | |||
| console.log(`✅ No-lot item completed: ${lot.itemName || lot.itemCode}`); | |||
| console.log(` No-lot item completed: ${lot.itemName || lot.itemCode}`); | |||
| const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0; | |||
| const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || ''; | |||
| const issueData: PickExecutionIssueData = { | |||
| type: "Do", // Delivery Order type | |||
| pickOrderId: pickOrderId, | |||
| pickOrderCode: pickOrderCode, | |||
| pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // ✅ Use dayjs format | |||
| pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format | |||
| pickExecutionDate: dayjs().format('YYYY-MM-DD'), | |||
| pickOrderLineId: lot.pickOrderLineId, | |||
| itemId: lot.itemId, | |||
| itemCode: lot.itemCode || '', | |||
| itemDescription: lot.itemName || '', | |||
| lotId: null, // ✅ No lot available | |||
| lotNo: null, // ✅ No lot number | |||
| lotId: null, // No lot available | |||
| lotNo: null, // No lot number | |||
| storeLocation: lot.location || '', | |||
| requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, | |||
| actualPickQty: 0, // ✅ No items picked (no lot available) | |||
| actualPickQty: 0, // No items picked (no lot available) | |||
| missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, | |||
| badItemQty: 0, | |||
| issueRemark: `No lot available for this item. Handled via handlelotnull.`, | |||
| @@ -1607,7 +1607,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return { success: true, lotNo: lot.lotNo || 'No Lot', isNoLot: true }; | |||
| } | |||
| // ✅ 正常情况:有 lot 的处理逻辑 | |||
| // 正常情况:有 lot 的处理逻辑 | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + submitQty; | |||
| @@ -1644,13 +1644,13 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return { success: true, lotNo: lot.lotNo }; | |||
| }); | |||
| // ✅ Wait for all submissions to complete | |||
| // 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`); | |||
| console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||
| // ✅ Refresh data once after all submissions | |||
| // Refresh data once after all submissions | |||
| await fetchAllCombinedLotData(); | |||
| if (successCount > 0) { | |||
| @@ -1669,11 +1669,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| } | |||
| }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, handlelotnull]); | |||
| // ✅ Calculate scanned items count | |||
| // ✅ Calculate scanned items count (should match handleSubmitAllScanned filter logic) | |||
| // Calculate scanned items count | |||
| // Calculate scanned items count (should match handleSubmitAllScanned filter logic) | |||
| const scannedItemsCount = useMemo(() => { | |||
| const filtered = combinedLotData.filter(lot => { | |||
| // ✅ 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含 | |||
| // 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含 | |||
| if (lot.noLot === true) { | |||
| const status = lot.stockOutLineStatus?.toLowerCase(); | |||
| const include = status !== 'completed' && status !== 'rejected'; | |||
| @@ -1682,11 +1682,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| } | |||
| return include; | |||
| } | |||
| // ✅ 正常情况:只包含 checked 状态 | |||
| // 正常情况:只包含 checked 状态 | |||
| return lot.stockOutLineStatus === 'checked'; | |||
| }); | |||
| // ✅ 添加调试日志 | |||
| // 添加调试日志 | |||
| const noLotCount = filtered.filter(l => l.noLot === true).length; | |||
| const normalCount = filtered.filter(l => l.noLot !== true).length; | |||
| console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`); | |||
| @@ -1699,7 +1699,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return filtered.length; | |||
| }, [combinedLotData]); | |||
| // ✅ ADD THIS: Auto-stop scan when no data available | |||
| // ADD THIS: Auto-stop scan when no data available | |||
| useEffect(() => { | |||
| if (isManualScanning && combinedLotData.length === 0) { | |||
| console.log("⏹️ No data available, auto-stopping QR scan..."); | |||
| @@ -1707,7 +1707,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| } | |||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||
| // ✅ Cleanup effect | |||
| // Cleanup effect | |||
| useEffect(() => { | |||
| return () => { | |||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||
| @@ -1754,7 +1754,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| {/* ✅ 保留:Combined Lot Table - 包含所有 QR 扫描功能 */} | |||
| {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */} | |||
| <Box> | |||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||
| <Typography variant="h6" gutterBottom sx={{ mb: 0 }}> | |||
| @@ -1784,7 +1784,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| </Button> | |||
| )} | |||
| {/* ✅ 保留:Submit All Scanned Button */} | |||
| {/* 保留:Submit All Scanned Button */} | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| @@ -1824,9 +1824,9 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| </Typography> | |||
| </Stack> | |||
| {/* ✅ 改进:三个字段显示在一起,使用表格式布局 */} | |||
| {/* ✅ 改进:三个字段合并显示 */} | |||
| {/* ✅ 改进:表格式显示每个 pick order */} | |||
| {/* 改进:三个字段显示在一起,使用表格式布局 */} | |||
| {/* 改进:三个字段合并显示 */} | |||
| {/* 改进:表格式显示每个 pick order */} | |||
| <Box sx={{ | |||
| p: 2, | |||
| backgroundColor: '#f5f5f5', | |||
| @@ -1861,7 +1861,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return <Typography variant="body2" color="text.secondary">-</Typography>; | |||
| } | |||
| // ✅ 使用与外部基本信息相同的样式 | |||
| // 使用与外部基本信息相同的样式 | |||
| return Array.from({ length: maxLength }, (_, idx) => ( | |||
| <Stack | |||
| key={idx} | |||
| @@ -1915,7 +1915,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| ) : ( | |||
| // 在第 1797-1938 行之间,将整个 map 函数修改为: | |||
| paginatedData.map((lot, index) => { | |||
| // ✅ 检查是否是 issue lot | |||
| // 检查是否是 issue lot | |||
| const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; | |||
| return ( | |||
| @@ -1961,7 +1961,7 @@ paginatedData.map((lot, index) => { | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {/* ✅ Issue lot 不显示扫描状态 */} | |||
| {/* Issue lot 不显示扫描状态 */} | |||
| {!isIssueLot && lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||
| <Checkbox | |||
| @@ -1996,7 +1996,7 @@ paginatedData.map((lot, index) => { | |||
| <TableCell align="center"> | |||
| <Box sx={{ display: 'flex', justifyContent: 'center' }}> | |||
| {isIssueLot ? ( | |||
| // ✅ Issue lot 只显示 Issue 按钮 | |||
| // Issue lot 只显示 Issue 按钮 | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| @@ -2017,7 +2017,7 @@ paginatedData.map((lot, index) => { | |||
| {t("Issue")} | |||
| </Button> | |||
| ) : ( | |||
| // ✅ Normal lot 显示两个按钮 | |||
| // Normal lot 显示两个按钮 | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Button | |||
| variant="contained" | |||
| @@ -2090,7 +2090,7 @@ paginatedData.map((lot, index) => { | |||
| </Box> | |||
| </Stack> | |||
| {/* ✅ 保留:QR Code Modal */} | |||
| {/* 保留:QR Code Modal */} | |||
| <QrCodeModal | |||
| open={qrModalOpen} | |||
| onClose={() => { | |||
| @@ -2104,7 +2104,7 @@ paginatedData.map((lot, index) => { | |||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | |||
| /> | |||
| {/* ✅ 保留:Lot Confirmation Modal */} | |||
| {/* 保留:Lot Confirmation Modal */} | |||
| {lotConfirmationOpen && expectedLotData && scannedLotData && ( | |||
| <LotConfirmationModal | |||
| open={lotConfirmationOpen} | |||
| @@ -2120,7 +2120,7 @@ paginatedData.map((lot, index) => { | |||
| /> | |||
| )} | |||
| {/* ✅ 保留:Good Pick Execution Form Modal */} | |||
| {/* 保留:Good Pick Execution Form Modal */} | |||
| {pickExecutionFormOpen && selectedLotForExecutionForm && ( | |||
| <GoodPickExecutionForm | |||
| open={pickExecutionFormOpen} | |||
| @@ -33,7 +33,7 @@ import EscalationComponent from "../PoDetail/EscalationComponent"; | |||
| import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; | |||
| import { | |||
| updateInventoryLotLineStatus | |||
| } from "@/app/api/inventory/actions"; // ✅ 导入新的 API | |||
| } from "@/app/api/inventory/actions"; // 导入新的 API | |||
| import { dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| @@ -42,8 +42,8 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| order?: number; // ✅ Add order property | |||
| stableId?: string; // ✅ Also add stableId for better row identification | |||
| order?: number; // Add order property | |||
| stableId?: string; // Also add stableId for better row identification | |||
| } | |||
| interface Props extends CommonProps { | |||
| itemDetail: GetPickOrderLineInfo & { | |||
| @@ -55,7 +55,7 @@ interface Props extends CommonProps { | |||
| selectedLotId?: number; | |||
| onStockOutLineUpdate?: () => void; | |||
| lotData: LotPickData[]; | |||
| // ✅ Add missing props | |||
| // Add missing props | |||
| pickQtyData?: PickQtyData; | |||
| selectedRowId?: number; | |||
| } | |||
| @@ -104,7 +104,7 @@ interface Props extends CommonProps { | |||
| }; | |||
| qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem | |||
| setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem | |||
| // ✅ Add props for stock out line update | |||
| // Add props for stock out line update | |||
| selectedLotId?: number; | |||
| onStockOutLineUpdate?: () => void; | |||
| lotData: LotPickData[]; | |||
| @@ -193,7 +193,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed | |||
| type: "pick_order_qc", | |||
| remarks: item.remarks || "", | |||
| qcPassed: item.isPassed, // ✅ This will now be included | |||
| qcPassed: item.isPassed, // This will now be included | |||
| })); | |||
| // Store the submitted data for debug display | |||
| @@ -217,17 +217,17 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| } | |||
| }; | |||
| // ✅ 修改:在组件开始时自动设置失败数量 | |||
| // 修改:在组件开始时自动设置失败数量 | |||
| useEffect(() => { | |||
| if (itemDetail && qcItems.length > 0 && selectedLotId) { | |||
| // ✅ 获取选中的批次数据 | |||
| // 获取选中的批次数据 | |||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||
| if (selectedLot) { | |||
| // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||
| // 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||
| const updatedQcItems = qcItems.map((item, index) => ({ | |||
| ...item, | |||
| failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty | |||
| // ✅ Add stable order and ID fields | |||
| // Add stable order and ID fields | |||
| order: index, | |||
| stableId: `qc-${item.id}-${index}` | |||
| })); | |||
| @@ -236,7 +236,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| } | |||
| }, [itemDetail, qcItems.length, selectedLotId, lotData]); | |||
| // ✅ Add this helper function at the top of the component | |||
| // Add this helper function at the top of the component | |||
| const safeClose = useCallback(() => { | |||
| if (onClose) { | |||
| // Create a mock event object that satisfies the Modal onClose signature | |||
| @@ -259,12 +259,12 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| isPersistent: () => false | |||
| } as any; | |||
| // ✅ Fixed: Pass both event and reason parameters | |||
| // Fixed: Pass both event and reason parameters | |||
| onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason | |||
| } | |||
| }, [onClose]); | |||
| // ✅ 修改:移除 alert 弹窗,改为控制台日志 | |||
| // 修改:移除 alert 弹窗,改为控制台日志 | |||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | |||
| async (data, event) => { | |||
| setIsSubmitting(true); | |||
| @@ -276,7 +276,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| const validationErrors : string[] = []; | |||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||
| // ✅ Add safety check for selectedLot | |||
| // Add safety check for selectedLot | |||
| if (!selectedLot) { | |||
| console.error("Selected lot not found"); | |||
| return; | |||
| @@ -313,23 +313,23 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| // ✅ Handle different QC decisions | |||
| // Handle different QC decisions | |||
| if (selectedLotId) { | |||
| try { | |||
| const allPassed = qcData.qcItems.every(item => item.isPassed); | |||
| if (qcDecision === "2") { | |||
| // ✅ QC Decision 2: Report and Re-pick | |||
| // QC Decision 2: Report and Re-pick | |||
| console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable"); | |||
| // ✅ Inventory lot line status: unavailable | |||
| // Inventory lot line status: unavailable | |||
| if (selectedLot) { | |||
| try { | |||
| console.log("=== DEBUG: Updating inventory lot line status ==="); | |||
| console.log("Selected lot:", selectedLot); | |||
| console.log("Selected lot ID:", selectedLotId); | |||
| // ✅ FIX: Only send the fields that the backend expects | |||
| // FIX: Only send the fields that the backend expects | |||
| const updateData = { | |||
| inventoryLotLineId: selectedLot.lotId, | |||
| status: 'unavailable' | |||
| @@ -339,7 +339,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| console.log("Update data:", updateData); | |||
| const result = await updateInventoryLotLineStatus(updateData); | |||
| console.log("✅ Inventory lot line status updated successfully:", result); | |||
| console.log(" Inventory lot line status updated successfully:", result); | |||
| } catch (error) { | |||
| console.error("❌ Error updating inventory lot line status:", error); | |||
| @@ -359,28 +359,28 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| // ✅ Close modal and refresh data | |||
| safeClose(); // ✅ Fixed: Use safe close function with both parameters | |||
| // Close modal and refresh data | |||
| safeClose(); // Fixed: Use safe close function with both parameters | |||
| if (onStockOutLineUpdate) { | |||
| onStockOutLineUpdate(); | |||
| } | |||
| } else if (qcDecision === "1") { | |||
| // ✅ QC Decision 1: Accept | |||
| // QC Decision 1: Accept | |||
| console.log("QC Decision 1 - Accept: QC passed"); | |||
| // ✅ Stock out line status: checked (QC completed) | |||
| // Stock out line status: checked (QC completed) | |||
| await updateStockOutLineStatus({ | |||
| id: selectedLotId, | |||
| status: 'checked', | |||
| qty: acceptQty || 0 | |||
| }); | |||
| // ✅ Inventory lot line status: NO CHANGE needed | |||
| // Inventory lot line status: NO CHANGE needed | |||
| // Keep the existing status from handleSubmitPickQty | |||
| // ✅ Close modal and refresh data | |||
| safeClose(); // ✅ Fixed: Use safe close function with both parameters | |||
| // Close modal and refresh data | |||
| safeClose(); // Fixed: Use safe close function with both parameters | |||
| if (onStockOutLineUpdate) { | |||
| onStockOutLineUpdate(); | |||
| } | |||
| @@ -399,7 +399,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| console.log("QC results saved successfully!"); | |||
| // ✅ Show warning dialog for failed QC items when accepting | |||
| // Show warning dialog for failed QC items when accepting | |||
| if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { | |||
| submitDialogWithWarning(() => { | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| @@ -448,7 +448,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = e.target.value === "true"; | |||
| // ✅ Simple state update | |||
| // Simple state update | |||
| setQcItems(prev => | |||
| prev.map(item => | |||
| item.id === params.id | |||
| @@ -490,10 +490,10 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | |||
| // 修改:失败项目自动显示 Lot Required Pick Qty | |||
| value={!params.row.qcPassed ? (0) : 0} | |||
| disabled={params.row.qcPassed} | |||
| // ✅ 移除 onChange,因为数量是固定的 | |||
| // 移除 onChange,因为数量是固定的 | |||
| // onChange={(e) => { | |||
| // const v = e.target.value; | |||
| // const next = v === "" ? undefined : Number(v); | |||
| @@ -535,7 +535,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| [t], | |||
| ); | |||
| // ✅ Add stable update function | |||
| // Add stable update function | |||
| const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => { | |||
| setQcItems(prevItems => | |||
| prevItems.map(item => | |||
| @@ -546,16 +546,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| ); | |||
| }, []); | |||
| // ✅ Remove duplicate functions | |||
| // Remove duplicate functions | |||
| const getRowId = useCallback((row: any) => { | |||
| return row.id; // Just use the original ID | |||
| }, []); | |||
| // ✅ Remove complex sorting logic | |||
| // Remove complex sorting logic | |||
| // const stableQcItems = useMemo(() => { ... }); // Remove | |||
| // const sortedQcItems = useMemo(() => { ... }); // Remove | |||
| // ✅ Use qcItems directly in DataGrid | |||
| // Use qcItems directly in DataGrid | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| @@ -593,9 +593,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| rows={qcItems} // ✅ Use qcItems directly | |||
| rows={qcItems} // Use qcItems directly | |||
| autoHeight | |||
| getRowId={getRowId} // ✅ Simple row ID function | |||
| getRowId={getRowId} // Simple row ID function | |||
| /> | |||
| </Grid> | |||
| </> | |||
| @@ -636,7 +636,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| /> | |||
| {/* ✅ Combirne options 2 & 3 into one */} | |||
| {/* Combirne options 2 & 3 into one */} | |||
| <FormControlLabel | |||
| value="2" | |||
| control={<Radio />} | |||
| @@ -649,7 +649,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| </FormControl> | |||
| </Grid> | |||
| {/* ✅ Show escalation component when QC Decision = 2 (Report and Re-pick) */} | |||
| {/* Show escalation component when QC Decision = 2 (Report and Re-pick) */} | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| @@ -568,7 +568,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| return; | |||
| } | |||
| // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||
| // 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||
| // if (!data.type) { | |||
| // alert(t("Please select product type")); | |||
| // return; | |||
| @@ -625,7 +625,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| } | |||
| } | |||
| // ✅ 修复:自动使用 "Consumable" 作为默认 type | |||
| // 修复:自动使用 "Consumable" 作为默认 type | |||
| const pickOrderData: SavePickOrderRequest = { | |||
| type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable" | |||
| targetDate: formattedTargetDate, | |||
| @@ -34,7 +34,7 @@ interface CombinedLotTableProps { | |||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||
| } | |||
| // ✅ Simple helper function to check if item is completed | |||
| // Simple helper function to check if item is completed | |||
| const isItemCompleted = (lot: any) => { | |||
| const actualPickQty = Number(lot.actualPickQty) || 0; | |||
| const requiredQty = Number(lot.requiredQty) || 0; | |||
| @@ -60,7 +60,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| // ✅ Paginated data | |||
| // Paginated data | |||
| const paginatedLotData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| @@ -113,7 +113,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| const isCompleted = isItemCompleted(lot); | |||
| const isRejected = isItemRejected(lot); | |||
| // ✅ Green text color for completed items | |||
| // Green text color for completed items | |||
| const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit'; | |||
| return ( | |||
| @@ -24,7 +24,7 @@ import { | |||
| Accordion, | |||
| AccordionSummary, | |||
| AccordionDetails, | |||
| Checkbox, // ✅ Add Checkbox import | |||
| Checkbox, // Add Checkbox import | |||
| } from "@mui/material"; | |||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||
| import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||
| @@ -47,7 +47,7 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ 修改:已完成的 Job Order Pick Order 接口 | |||
| // 修改:已完成的 Job Order Pick Order 接口 | |||
| interface CompletedJobOrderPickOrder { | |||
| id: number; | |||
| pickOrderId: number; | |||
| @@ -68,7 +68,7 @@ interface CompletedJobOrderPickOrder { | |||
| completedItems: number; | |||
| } | |||
| // ✅ 新增:Lot 详情接口 | |||
| // 新增:Lot 详情接口 | |||
| interface LotDetail { | |||
| lotId: number; | |||
| lotNo: string; | |||
| @@ -104,21 +104,21 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 修改:已完成 Job Order Pick Orders 状态 | |||
| // 修改:已完成 Job Order Pick Orders 状态 | |||
| const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false); | |||
| // ✅ 修改:详情视图状态 | |||
| // 修改:详情视图状态 | |||
| const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null); | |||
| const [showDetailView, setShowDetailView] = useState(false); | |||
| const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]); | |||
| const [detailLotDataLoading, setDetailLotDataLoading] = useState(false); | |||
| // ✅ 修改:搜索状态 | |||
| // 修改:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| // ✅ 修改:分页状态 | |||
| // 修改:分页状态 | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| @@ -127,7 +127,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders | |||
| // 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders | |||
| const fetchCompletedJobOrderPickOrdersData = useCallback(async () => { | |||
| if (!currentUserId) return; | |||
| @@ -139,7 +139,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| setCompletedJobOrderPickOrders(completedJobOrderPickOrders); | |||
| setFilteredJobOrderPickOrders(completedJobOrderPickOrders); | |||
| console.log("✅ Fetched completed Job Order pick orders:", completedJobOrderPickOrders); | |||
| console.log(" Fetched completed Job Order pick orders:", completedJobOrderPickOrders); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching completed Job Order pick orders:", error); | |||
| setCompletedJobOrderPickOrders([]); | |||
| @@ -149,7 +149,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 新增:获取 lot 详情数据 | |||
| // 新增:获取 lot 详情数据 | |||
| const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { | |||
| setDetailLotDataLoading(true); | |||
| try { | |||
| @@ -158,7 +158,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId); | |||
| setDetailLotData(lotDetails); | |||
| console.log("✅ Fetched lot details:", lotDetails); | |||
| console.log(" Fetched lot details:", lotDetails); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching lot details:", error); | |||
| setDetailLotData([]); | |||
| @@ -167,14 +167,14 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, []); | |||
| // ✅ 修改:初始化时获取数据 | |||
| // 修改:初始化时获取数据 | |||
| useEffect(() => { | |||
| if (currentUserId) { | |||
| fetchCompletedJobOrderPickOrdersData(); | |||
| } | |||
| }, [currentUserId, fetchCompletedJobOrderPickOrdersData]); | |||
| // ✅ 修改:搜索功能 | |||
| // 修改:搜索功能 | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| @@ -196,13 +196,13 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Filtered Job Order pick orders count:", filtered.length); | |||
| }, [completedJobOrderPickOrders]); | |||
| // ✅ 修改:重置搜索 | |||
| // 修改:重置搜索 | |||
| const handleSearchReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| setFilteredJobOrderPickOrders(completedJobOrderPickOrders); | |||
| }, [completedJobOrderPickOrders]); | |||
| // ✅ 修改:分页功能 | |||
| // 修改:分页功能 | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| ...prev, | |||
| @@ -218,14 +218,14 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| }, []); | |||
| // ✅ 修改:分页数据 | |||
| // 修改:分页数据 | |||
| const paginatedData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| return filteredJobOrderPickOrders.slice(startIndex, endIndex); | |||
| }, [filteredJobOrderPickOrders, paginationController]); | |||
| // ✅ 修改:搜索条件 | |||
| // 修改:搜索条件 | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| @@ -244,34 +244,34 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| ]; | |||
| // ✅ 修改:详情点击处理 | |||
| // 修改:详情点击处理 | |||
| const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { | |||
| setSelectedJobOrderPickOrder(jobOrderPickOrder); | |||
| setShowDetailView(true); | |||
| // ✅ 获取 lot 详情数据 | |||
| // 获取 lot 详情数据 | |||
| await fetchLotDetailsData(jobOrderPickOrder.pickOrderId); | |||
| // ✅ 触发打印按钮状态更新 - 基于详情数据 | |||
| // 触发打印按钮状态更新 - 基于详情数据 | |||
| const allCompleted = jobOrderPickOrder.secondScanCompleted; | |||
| // ✅ 发送事件,包含标签页信息 | |||
| // 发送事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件 | |||
| tabIndex: 2 // 明确指定这是来自标签页 2 的事件 | |||
| } | |||
| })); | |||
| }, [fetchLotDetailsData]); | |||
| // ✅ 修改:返回列表视图 | |||
| // 修改:返回列表视图 | |||
| const handleBackToList = useCallback(() => { | |||
| setShowDetailView(false); | |||
| setSelectedJobOrderPickOrder(null); | |||
| setDetailLotData([]); | |||
| // ✅ 返回列表时禁用打印按钮 | |||
| // 返回列表时禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -280,7 +280,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| })); | |||
| }, []); | |||
| // ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | |||
| // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | |||
| if (showDetailView && selectedJobOrderPickOrder) { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| @@ -322,7 +322,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| </CardContent> | |||
| </Card> | |||
| {/* ✅ 修改:Lot 详情表格 - 添加复选框列 */} | |||
| {/* 修改:Lot 详情表格 - 添加复选框列 */} | |||
| <Card> | |||
| <CardContent> | |||
| <Typography variant="h6" gutterBottom> | |||
| @@ -353,7 +353,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableBody> | |||
| {detailLotData.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={10} align="center"> {/* ✅ 恢复原来的 colSpan */} | |||
| <TableCell colSpan={10} align="center"> {/* 恢复原来的 colSpan */} | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No lot details available")} | |||
| </Typography> | |||
| @@ -382,7 +382,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableCell align="right"> | |||
| {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||
| </TableCell> | |||
| {/* ✅ 修改:Processing Status 使用复选框 */} | |||
| {/* 修改:Processing Status 使用复选框 */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ | |||
| display: 'flex', | |||
| @@ -409,7 +409,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| /> | |||
| </Box> | |||
| </TableCell> | |||
| {/* ✅ 修改:Second Scan Status 使用复选框 */} | |||
| {/* 修改:Second Scan Status 使用复选框 */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ | |||
| display: 'flex', | |||
| @@ -450,7 +450,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| ); | |||
| } | |||
| // ✅ 修改:默认列表视图 | |||
| // 修改:默认列表视图 | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Box> | |||
| @@ -36,7 +36,7 @@ import { | |||
| checkAndCompletePickOrderByConsoCode, | |||
| confirmLotSubstitution | |||
| } from "@/app/api/pickOrder/actions"; | |||
| // ✅ 修改:使用 Job Order API | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| fetchJobOrderLotsHierarchical, | |||
| fetchUnassignedJobOrderPickOrders, | |||
| @@ -62,7 +62,7 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ QR Code Modal Component (from GoodPickExecution) | |||
| // QR Code Modal Component (from GoodPickExecution) | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| onClose: () => void; | |||
| @@ -108,7 +108,7 @@ const QrCodeModal: React.FC<{ | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| if (stockInLineInfo.lotNo === lot.lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lot.lotNo}`); | |||
| setQrScanSuccess(true); | |||
| onQrCodeSubmit(lot.lotNo); | |||
| onClose(); | |||
| @@ -300,7 +300,7 @@ const QrCodeModal: React.FC<{ | |||
| {qrScanSuccess && ( | |||
| <Typography variant="caption" color="success" display="block"> | |||
| ✅ {t("Verified successfully!")} | |||
| {t("Verified successfully!")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| @@ -323,13 +323,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 修改:使用 Job Order 数据结构 | |||
| // 修改:使用 Job Order 数据结构 | |||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||
| // ✅ 添加未分配订单状态 | |||
| // 添加未分配订单状态 | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| @@ -359,23 +359,23 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const errors = formProps.formState.errors; | |||
| const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false); | |||
| // ✅ Add QR modal states | |||
| // Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | |||
| // ✅ Add GoodPickExecutionForm states | |||
| // Add GoodPickExecutionForm states | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | |||
| const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]); | |||
| const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false); | |||
| // ✅ Add these missing state variables | |||
| // Add these missing state variables | |||
| const [isManualScanning, setIsManualScanning] = useState<boolean>(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| // ✅ 修改:加载未分配的 Job Order 订单 | |||
| // 修改:加载未分配的 Job Order 订单 | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| try { | |||
| @@ -388,7 +388,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, []); | |||
| // ✅ 修改:分配订单给当前用户 | |||
| // 修改:分配订单给当前用户 | |||
| const handleAssignOrder = useCallback(async (pickOrderId: number) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| @@ -398,7 +398,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| try { | |||
| const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||
| if (result.message === "Successfully assigned") { | |||
| console.log("✅ Successfully assigned pick order"); | |||
| console.log(" Successfully assigned pick order"); | |||
| // 刷新数据 | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| // 重新加载未分配订单列表 | |||
| @@ -437,7 +437,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const allFgPickOrders = fgPickOrdersResults.flat(); | |||
| setFgPickOrders(allFgPickOrders); | |||
| console.log("✅ Fetched FG pick orders:", allFgPickOrders); | |||
| console.log(" Fetched FG pick orders:", allFgPickOrders); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching FG pick orders:", error); | |||
| setFgPickOrders([]); | |||
| @@ -452,13 +452,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [combinedLotData, fetchFgPickOrdersData]); | |||
| // ✅ Handle QR code button click | |||
| // 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 获取数据 | |||
| // 修改:使用 Job Order API 获取数据 | |||
| const fetchJobOrderData = useCallback(async (userId?: number) => { | |||
| setCombinedDataLoading(true); | |||
| try { | |||
| @@ -479,13 +479,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // ✅ 使用 Job Order API | |||
| // 使用 Job Order API | |||
| const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); | |||
| console.log("✅ Job Order data:", jobOrderData); | |||
| console.log(" Job Order data:", jobOrderData); | |||
| setJobOrderData(jobOrderData); | |||
| // ✅ Transform hierarchical data to flat structure for the table | |||
| // Transform hierarchical data to flat structure for the table | |||
| const flatLotData: any[] = []; | |||
| if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { | |||
| @@ -541,7 +541,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| } | |||
| console.log("✅ Transformed flat lot data:", flatLotData); | |||
| console.log(" Transformed flat lot data:", flatLotData); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| const hasData = flatLotData.length > 0; | |||
| @@ -551,16 +551,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // ✅ 计算完成状态并发送事件 | |||
| // 计算完成状态并发送事件 | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| // ✅ 发送完成状态事件,包含标签页信息 | |||
| // 发送完成状态事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件 | |||
| tabIndex: 0 // 明确指定这是来自标签页 0 的事件 | |||
| } | |||
| })); | |||
| @@ -570,7 +570,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| // ✅ 如果加载失败,禁用打印按钮 | |||
| // 如果加载失败,禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -582,10 +582,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 修改:初始化时加载数据 | |||
| // 修改:初始化时加载数据 | |||
| useEffect(() => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| console.log("✅ Session loaded, initializing job order..."); | |||
| console.log(" Session loaded, initializing job order..."); | |||
| initializationRef.current = true; | |||
| // 加载 Job Order 数据 | |||
| @@ -595,7 +595,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); | |||
| // ✅ Add event listener for manual assignment | |||
| // Add event listener for manual assignment | |||
| useEffect(() => { | |||
| const handlePickOrderAssigned = () => { | |||
| console.log("🔄 Pick order assigned event received, refreshing data..."); | |||
| @@ -609,11 +609,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }; | |||
| }, [fetchJobOrderData]); | |||
| // ✅ Handle QR code submission for matched lot (external scanning) | |||
| // Handle QR code submission for matched lot (external scanning) | |||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||
| console.log(`✅ Processing QR Code for lot: ${lotNo}`); | |||
| console.log(` Processing QR Code for lot: ${lotNo}`); | |||
| // ✅ Use current data without refreshing to avoid infinite loop | |||
| // Use current data without refreshing to avoid infinite loop | |||
| const currentLotData = combinedLotData; | |||
| console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo)); | |||
| @@ -631,7 +631,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return; | |||
| } | |||
| console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| console.log(` Found ${matchingLots.length} matching lots:`, matchingLots); | |||
| setQrScanError(false); | |||
| try { | |||
| @@ -706,13 +706,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| } | |||
| // ✅ FIXED: Set refresh flag before refreshing data | |||
| // FIXED: Set refresh flag before refreshing data | |||
| setIsRefreshingData(true); | |||
| console.log("🔄 Refreshing data after QR code processing..."); | |||
| await fetchJobOrderData(); | |||
| if (successCount > 0) { | |||
| console.log(`✅ QR Code processing completed: ${successCount} updated/created`); | |||
| console.log(` QR Code processing completed: ${successCount} updated/created`); | |||
| setQrScanSuccess(true); | |||
| setQrScanError(false); | |||
| setQrScanInput(''); // Clear input after successful processing | |||
| @@ -727,11 +727,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| // ✅ Still refresh data even on error | |||
| // Still refresh data even on error | |||
| setIsRefreshingData(true); | |||
| await fetchJobOrderData(); | |||
| } finally { | |||
| // ✅ Clear refresh flag after a short delay | |||
| // Clear refresh flag after a short delay | |||
| setTimeout(() => { | |||
| setIsRefreshingData(false); | |||
| }, 1000); | |||
| @@ -744,7 +744,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setLotConfirmationOpen(true); | |||
| }, []); | |||
| // ✅ Add handleLotConfirmation function | |||
| // Add handleLotConfirmation function | |||
| const handleLotConfirmation = useCallback(async () => { | |||
| if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; | |||
| setIsConfirmingLot(true); | |||
| @@ -767,7 +767,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Lot ID (fallback):", selectedLotForQr.lotId); | |||
| console.log("New Inventory Lot Line ID:", newLotLineId); | |||
| // ✅ Call confirmLotSubstitution to update the suggested lot | |||
| // Call confirmLotSubstitution to update the suggested lot | |||
| const substitutionResult = await confirmLotSubstitution({ | |||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | |||
| stockOutLineId: selectedLotForQr.stockOutLineId, | |||
| @@ -775,52 +775,52 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| newInventoryLotLineId: newLotLineId | |||
| }); | |||
| console.log("✅ Lot substitution result:", substitutionResult); | |||
| console.log(" Lot substitution result:", substitutionResult); | |||
| // ✅ Update stock out line status to 'checked' after substitution | |||
| // 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'"); | |||
| console.log(" Stock out line status updated to 'checked'"); | |||
| } | |||
| // ✅ Close modal and clean up state BEFORE refreshing | |||
| // 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 | |||
| // Clear QR processing state but DON'T clear processedQrCodes yet | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| setQrScanInput(''); | |||
| // ✅ Set refreshing flag to prevent QR processing during refresh | |||
| // Set refreshing flag to prevent QR processing during refresh | |||
| setIsRefreshingData(true); | |||
| // ✅ Refresh data to show updated lot | |||
| // Refresh data to show updated lot | |||
| console.log("🔄 Refreshing job order data..."); | |||
| await fetchJobOrderData(); | |||
| console.log("✅ Lot substitution confirmed and data refreshed"); | |||
| console.log(" Lot substitution confirmed and data refreshed"); | |||
| // ✅ Clear processed QR codes and flags immediately after refresh | |||
| // 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"); | |||
| 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 | |||
| }, 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 | |||
| // Clear refresh flag on error | |||
| setIsRefreshingData(false); | |||
| } finally { | |||
| setIsConfirmingLot(false); | |||
| @@ -828,7 +828,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); | |||
| const processOutsideQrCode = useCallback(async (latestQr: string) => { | |||
| // ✅ Don't process if confirmation modal is open | |||
| // Don't process if confirmation modal is open | |||
| if (lotConfirmationOpen) { | |||
| console.log("⏸️ Confirmation modal is open, skipping QR processing"); | |||
| return; | |||
| @@ -857,7 +857,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return; | |||
| } | |||
| // ✅ First, fetch stock in line info to get the lot number | |||
| // First, fetch stock in line info to get the lot number | |||
| let stockInLineInfo: any; | |||
| try { | |||
| stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId); | |||
| @@ -923,7 +923,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // 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}`); | |||
| console.log(` Exact lot match: ${scanned.lotNo}`); | |||
| await handleQrCodeSubmit(scanned.lotNo); | |||
| return; | |||
| } | |||
| @@ -931,7 +931,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // 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 | |||
| // DON'T stop scanning - just pause QR processing by showing modal | |||
| setSelectedLotForQr(expectedLot); | |||
| handleLotMismatch( | |||
| { | |||
| @@ -962,10 +962,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [qrScanInput, handleQrCodeSubmit]); | |||
| // ✅ Handle QR code submission from modal (internal scanning) | |||
| // 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}`); | |||
| console.log(` QR Code verified for lot: ${lotNo}`); | |||
| const requiredQty = selectedLotForQr.requiredQty; | |||
| const lotId = selectedLotForQr.lotId; | |||
| @@ -993,7 +993,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| ...prev, | |||
| [lotKey]: requiredQty | |||
| })); | |||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| }, 500); | |||
| // Refresh data | |||
| @@ -1006,7 +1006,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| useEffect(() => { | |||
| // ✅ Add isManualScanning check | |||
| // Add isManualScanning check | |||
| if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) { | |||
| return; | |||
| } | |||
| @@ -1064,7 +1064,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) { | |||
| console.log("Found completed pick orders, auto-assigning next..."); | |||
| // ✅ 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // 移除前端的自动分配逻辑,因为后端已经处理了 | |||
| // await handleAutoAssignAndRelease(); // 删除这个函数 | |||
| } | |||
| } catch (error) { | |||
| @@ -1072,7 +1072,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ Handle submit pick quantity | |||
| // Handle submit pick quantity | |||
| const handleSubmitPickQty = useCallback(async (lot: any) => { | |||
| const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; | |||
| const newQty = pickQtyData[lotKey] || 0; | |||
| @@ -1116,14 +1116,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| } | |||
| // ✅ FIXED: Use the proper API function instead of direct fetch | |||
| // 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...`); | |||
| console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`); | |||
| try { | |||
| // ✅ Use the imported API function instead of direct fetch | |||
| // Use the imported API function instead of direct fetch | |||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); | |||
| console.log(`✅ Pick order completion check result:`, completionResponse); | |||
| console.log(` Pick order completion check result:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); | |||
| @@ -1155,11 +1155,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| try { | |||
| // ✅ FIXED: Calculate cumulative quantity correctly | |||
| // FIXED: Calculate cumulative quantity correctly | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + submitQty; | |||
| // ✅ FIXED: Determine status based on cumulative quantity vs required quantity | |||
| // FIXED: Determine status based on cumulative quantity vs required quantity | |||
| let newStatus = 'partially_completed'; | |||
| if (cumulativeQty >= lot.requiredQty) { | |||
| @@ -1182,7 +1182,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: newStatus, | |||
| qty: cumulativeQty // ✅ Use cumulative quantity | |||
| qty: cumulativeQty // Use cumulative quantity | |||
| }); | |||
| if (submitQty > 0) { | |||
| @@ -1194,13 +1194,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| } | |||
| // ✅ Check if pick order is completed when lot status becomes 'completed' | |||
| // 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...`); | |||
| 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); | |||
| console.log(` Pick order completion check result:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); | |||
| @@ -1239,7 +1239,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||
| try { | |||
| // ✅ Submit all items in parallel using Promise.all | |||
| // 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; | |||
| @@ -1277,13 +1277,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return { success: true, lotNo: lot.lotNo }; | |||
| }); | |||
| // ✅ Wait for all submissions to complete | |||
| // 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`); | |||
| console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||
| // ✅ Refresh data once after all submissions | |||
| // Refresh data once after all submissions | |||
| await fetchJobOrderData(); | |||
| if (successCount > 0) { | |||
| @@ -1302,11 +1302,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||
| // ✅ Calculate scanned items count | |||
| // Calculate scanned items count | |||
| const scannedItemsCount = useMemo(() => { | |||
| return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length; | |||
| }, [combinedLotData]); | |||
| // ✅ Handle reject lot | |||
| // Handle reject lot | |||
| const handleRejectLot = useCallback(async (lot: any) => { | |||
| if (!lot.stockOutLineId) { | |||
| console.error("No stock out line found for this lot"); | |||
| @@ -1332,7 +1332,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | |||
| // ✅ Handle pick execution form | |||
| // Handle pick execution form | |||
| const handlePickExecutionForm = useCallback((lot: any) => { | |||
| console.log("=== Pick Execution Form ==="); | |||
| console.log("Lot data:", lot); | |||
| @@ -1362,7 +1362,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Pick execution issue recorded:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ Pick execution issue recorded successfully"); | |||
| console.log(" Pick execution issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| @@ -1376,7 +1376,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [fetchJobOrderData]); | |||
| // ✅ Calculate remaining required quantity | |||
| // Calculate remaining required quantity | |||
| const calculateRemainingRequiredQty = useCallback((lot: any) => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||
| @@ -1457,7 +1457,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // Pagination data with sorting by routerIndex | |||
| const paginatedData = useMemo(() => { | |||
| // ✅ Sort by routerIndex first, then by other criteria | |||
| // Sort by routerIndex first, then by other criteria | |||
| const sortedData = [...combinedLotData].sort((a, b) => { | |||
| const aIndex = a.routerIndex || 0; | |||
| const bIndex = b.routerIndex || 0; | |||
| @@ -1481,7 +1481,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return sortedData.slice(startIndex, endIndex); | |||
| }, [combinedLotData, paginationController]); | |||
| // ✅ Add these functions for manual scanning | |||
| // Add these functions for manual scanning | |||
| const handleStartScan = useCallback(() => { | |||
| console.log(" Starting manual QR scan..."); | |||
| setIsManualScanning(true); | |||
| @@ -1517,7 +1517,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||
| // ✅ Cleanup effect | |||
| // Cleanup effect | |||
| useEffect(() => { | |||
| return () => { | |||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||
| @@ -1605,7 +1605,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| {t("Stop QR Scan")} | |||
| </Button> | |||
| )} | |||
| {/* ✅ ADD THIS: Submit All Scanned Button */} | |||
| {/* ADD THIS: Submit All Scanned Button */} | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| @@ -1750,7 +1750,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || | |||
| lot.stockOutLineStatus === 'completed' || | |||
| lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed | |||
| lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed | |||
| } | |||
| sx={{ | |||
| fontSize: '0.75rem', | |||
| @@ -1770,8 +1770,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| (lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || | |||
| lot.stockOutLineStatus === 'completed' || // ✅ Disable when finished | |||
| lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed | |||
| lot.stockOutLineStatus === 'completed' || // Disable when finished | |||
| lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed | |||
| } | |||
| sx={{ | |||
| fontSize: '0.7rem', | |||
| @@ -1811,7 +1811,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| </Box> | |||
| </Stack> | |||
| {/* ✅ QR Code Modal */} | |||
| {/* QR Code Modal */} | |||
| {!lotConfirmationOpen && ( | |||
| <QrCodeModal | |||
| open={qrModalOpen} | |||
| @@ -1826,7 +1826,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | |||
| /> | |||
| )} | |||
| {/* ✅ Add Lot Confirmation Modal */} | |||
| {/* Add Lot Confirmation Modal */} | |||
| {lotConfirmationOpen && expectedLotData && scannedLotData && ( | |||
| <LotConfirmationModal | |||
| open={lotConfirmationOpen} | |||
| @@ -1843,7 +1843,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| isLoading={isConfirmingLot} | |||
| /> | |||
| )} | |||
| {/* ✅ Pick Execution Form Modal */} | |||
| {/* Pick Execution Form Modal */} | |||
| {pickExecutionFormOpen && selectedLotForExecutionForm && ( | |||
| <GoodPickExecutionForm | |||
| open={pickExecutionFormOpen} | |||
| @@ -1859,13 +1859,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| itemCode: selectedLotForExecutionForm.itemCode, | |||
| itemName: selectedLotForExecutionForm.itemName, | |||
| pickOrderCode: selectedLotForExecutionForm.pickOrderCode, | |||
| // ✅ Add missing required properties from GetPickOrderLineInfo interface | |||
| // 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 | |||
| pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty | |||
| suggestedList: [] // Add required suggestedList property | |||
| }} | |||
| pickOrderId={selectedLotForExecutionForm.pickOrderId} | |||
| pickOrderCreateDate={new Date()} | |||
| @@ -54,7 +54,7 @@ interface PickExecutionFormProps { | |||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||
| pickOrderId?: number; | |||
| pickOrderCreateDate: any; | |||
| // ✅ Remove these props since we're not handling normal cases | |||
| // Remove these props since we're not handling normal cases | |||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||
| // selectedRowId?: number | null; | |||
| } | |||
| @@ -76,7 +76,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| selectedPickOrderLine, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| // ✅ Remove these props | |||
| // Remove these props | |||
| // onNormalPickSubmit, | |||
| // selectedRowId, | |||
| }) => { | |||
| @@ -91,7 +91,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| return lot.availableQty || 0; | |||
| }, []); | |||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| // ✅ Use the original required quantity, not subtracting actualPickQty | |||
| // Use the original required quantity, not subtracting actualPickQty | |||
| // The actualPickQty in the form should be independent of the database value | |||
| return lot.requiredQty || 0; | |||
| }, []); | |||
| @@ -126,7 +126,7 @@ useEffect(() => { | |||
| } | |||
| }; | |||
| // ✅ Initialize verified quantity to the received quantity (actualPickQty) | |||
| // Initialize verified quantity to the received quantity (actualPickQty) | |||
| const initialVerifiedQty = selectedLot.actualPickQty || 0; | |||
| setVerifiedQty(initialVerifiedQty); | |||
| @@ -155,14 +155,14 @@ useEffect(() => { | |||
| handledBy: undefined, | |||
| }); | |||
| } | |||
| // ✅ 只在 open 状态改变时重新初始化,移除其他依赖 | |||
| // 只在 open 状态改变时重新初始化,移除其他依赖 | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [open]); | |||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | |||
| setFormData(prev => ({ ...prev, [field]: value })); | |||
| // ✅ Update verified quantity state when actualPickQty changes | |||
| // Update verified quantity state when actualPickQty changes | |||
| if (field === 'actualPickQty') { | |||
| setVerifiedQty(value); | |||
| } | |||
| @@ -173,7 +173,7 @@ useEffect(() => { | |||
| } | |||
| }, [errors]); | |||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| // Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| @@ -185,16 +185,16 @@ useEffect(() => { | |||
| newErrors.actualPickQty = t('Qty is required'); | |||
| } | |||
| // ✅ 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0 | |||
| // 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0 | |||
| // if (verifiedQty > receivedQty) { ... } ← 删除 | |||
| // ✅ 只检查总和是否等于需求数量 | |||
| // 只检查总和是否等于需求数量 | |||
| const totalQty = verifiedQty + badItemQty + missQty; | |||
| if (totalQty !== requiredQty) { | |||
| newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | |||
| } | |||
| // ✅ Require either missQty > 0 OR badItemQty > 0 | |||
| // Require either missQty > 0 OR badItemQty > 0 | |||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||
| @@ -213,7 +213,7 @@ useEffect(() => { | |||
| setLoading(true); | |||
| try { | |||
| // ✅ Use the verified quantity in the submission | |||
| // Use the verified quantity in the submission | |||
| const submissionData = { | |||
| ...formData, | |||
| actualPickQty: verifiedQty, | |||
| @@ -249,11 +249,11 @@ useEffect(() => { | |||
| return ( | |||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||
| <DialogTitle> | |||
| {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} | |||
| {t('Pick Execution Issue Form')} {/* Always show issue form title */} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mt: 2 }}> | |||
| {/* ✅ Add instruction text */} | |||
| {/* Add instruction text */} | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}> | |||
| @@ -263,7 +263,7 @@ useEffect(() => { | |||
| </Box> | |||
| </Grid> | |||
| {/* ✅ Keep the existing form fields */} | |||
| {/* Keep the existing form fields */} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| @@ -297,7 +297,7 @@ useEffect(() => { | |||
| // handleInputChange('actualPickQty', newValue); | |||
| }} | |||
| error={!!errors.actualPickQty} | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // ✅ 使用原始接收数量 | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量 | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -311,7 +311,7 @@ useEffect(() => { | |||
| onChange={(e) => { | |||
| const newMissQty = parseFloat(e.target.value) || 0; | |||
| handleInputChange('missQty', newMissQty); | |||
| // ✅ 不要自动修改其他字段 | |||
| // 不要自动修改其他字段 | |||
| }} | |||
| error={!!errors.missQty} | |||
| helperText={errors.missQty} | |||
| @@ -328,7 +328,7 @@ useEffect(() => { | |||
| onChange={(e) => { | |||
| const newBadItemQty = parseFloat(e.target.value) || 0; | |||
| handleInputChange('badItemQty', newBadItemQty); | |||
| // ✅ 不要自动修改其他字段 | |||
| // 不要自动修改其他字段 | |||
| }} | |||
| error={!!errors.badItemQty} | |||
| helperText={errors.badItemQty} | |||
| @@ -336,7 +336,7 @@ useEffect(() => { | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {/* Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| @@ -349,7 +349,7 @@ useEffect(() => { | |||
| value={formData.issueRemark || ''} | |||
| onChange={(e) => { | |||
| handleInputChange('issueRemark', e.target.value); | |||
| // ✅ Don't reset badItemQty when typing in issue remark | |||
| // Don't reset badItemQty when typing in issue remark | |||
| }} | |||
| error={!!errors.issueRemark} | |||
| helperText={errors.issueRemark} | |||
| @@ -365,7 +365,7 @@ useEffect(() => { | |||
| value={formData.handledBy ? formData.handledBy.toString() : ''} | |||
| onChange={(e) => { | |||
| handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined); | |||
| // ✅ Don't reset badItemQty when selecting handler | |||
| // Don't reset badItemQty when selecting handler | |||
| }} | |||
| label={t('handler')} | |||
| > | |||
| @@ -24,7 +24,7 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useRouter } from "next/navigation"; | |||
| // ✅ 修改:使用 Job Order API | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| fetchCompletedJobOrderPickOrders, | |||
| fetchUnassignedJobOrderPickOrders, | |||
| @@ -54,7 +54,7 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ QR Code Modal Component (from GoodPickExecution) | |||
| // QR Code Modal Component (from GoodPickExecution) | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| onClose: () => void; | |||
| @@ -101,7 +101,7 @@ const QrCodeModal: React.FC<{ | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| if (stockInLineInfo.lotNo === lot.lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lot.lotNo}`); | |||
| setQrScanSuccess(true); | |||
| onQrCodeSubmit(lot.lotNo); | |||
| onClose(); | |||
| @@ -293,7 +293,7 @@ const QrCodeModal: React.FC<{ | |||
| {qrScanSuccess && ( | |||
| <Typography variant="caption" color="success" display="block"> | |||
| ✅ {t("Verified successfully!")} | |||
| {t("Verified successfully!")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| @@ -316,13 +316,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 修改:使用 Job Order 数据结构 | |||
| // 修改:使用 Job Order 数据结构 | |||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||
| // ✅ 添加未分配订单状态 | |||
| // 添加未分配订单状态 | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| @@ -348,20 +348,20 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ Add QR modal states | |||
| // Add QR modal states | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null); | |||
| // ✅ Add GoodPickExecutionForm states | |||
| // Add GoodPickExecutionForm states | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null); | |||
| // ✅ Add these missing state variables | |||
| // Add these missing state variables | |||
| const [isManualScanning, setIsManualScanning] = useState<boolean>(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| // ✅ 修改:加载未分配的 Job Order 订单 | |||
| // 修改:加载未分配的 Job Order 订单 | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| try { | |||
| @@ -374,7 +374,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, []); | |||
| // ✅ 修改:分配订单给当前用户 | |||
| // 修改:分配订单给当前用户 | |||
| const handleAssignOrder = useCallback(async (pickOrderId: number) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| @@ -384,7 +384,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| try { | |||
| const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||
| if (result.message === "Successfully assigned") { | |||
| console.log("✅ Successfully assigned pick order"); | |||
| console.log(" Successfully assigned pick order"); | |||
| // 刷新数据 | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| // 重新加载未分配订单列表 | |||
| @@ -400,13 +400,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, [currentUserId, loadUnassignedOrders]); | |||
| // ✅ Handle QR code button click | |||
| // 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 获取数据 | |||
| // 修改:使用 Job Order API 获取数据 | |||
| const fetchJobOrderData = useCallback(async (userId?: number) => { | |||
| setCombinedDataLoading(true); | |||
| try { | |||
| @@ -427,15 +427,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| tabIndex: 1 | |||
| } | |||
| })); | |||
| // ✅ 使用 Job Order API | |||
| // 使用 Job Order API | |||
| const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse); | |||
| console.log("✅ Job Order data:", jobOrderData); | |||
| console.log("✅ Pick Order Code from API:", jobOrderData.pickOrder?.code); | |||
| console.log("✅ Expected Pick Order Code: P-20251003-001"); | |||
| console.log(" Job Order data:", jobOrderData); | |||
| console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code); | |||
| console.log(" Expected Pick Order Code: P-20251003-001"); | |||
| setJobOrderData(jobOrderData); | |||
| // ✅ Transform hierarchical data to flat structure for the table | |||
| // Transform hierarchical data to flat structure for the table | |||
| const flatLotData: any[] = []; | |||
| if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { | |||
| @@ -491,7 +491,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| } | |||
| console.log("✅ Transformed flat lot data:", flatLotData); | |||
| console.log(" Transformed flat lot data:", flatLotData); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| const hasData = flatLotData.length > 0; | |||
| @@ -501,7 +501,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| tabIndex: 1 | |||
| } | |||
| })); | |||
| // ✅ 计算完成状态并发送事件 | |||
| // 计算完成状态并发送事件 | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| @@ -511,11 +511,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| tabIndex: 1 | |||
| } | |||
| })); | |||
| // ✅ 发送完成状态事件,包含标签页信息 | |||
| // 发送完成状态事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件 | |||
| tabIndex: 0 // 明确指定这是来自标签页 0 的事件 | |||
| } | |||
| })); | |||
| @@ -525,7 +525,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| // ✅ 如果加载失败,禁用打印按钮 | |||
| // 如果加载失败,禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -550,7 +550,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||
| try { | |||
| // ✅ Submit all items in parallel using Promise.all | |||
| // Submit all items in parallel using Promise.all | |||
| const submitPromises = scannedLots.map(async (lot) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||
| @@ -570,13 +570,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return { success: result.code === "SUCCESS", itemCode: lot.itemCode }; | |||
| }); | |||
| // ✅ Wait for all submissions to complete | |||
| // 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`); | |||
| console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||
| // ✅ Refresh data once after all submissions | |||
| // Refresh data once after all submissions | |||
| await fetchJobOrderData(); | |||
| if (successCount > 0) { | |||
| @@ -592,15 +592,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData]); | |||
| // ✅ Calculate scanned items count | |||
| // Calculate scanned items count | |||
| const scannedItemsCount = useMemo(() => { | |||
| return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length; | |||
| }, [combinedLotData]); | |||
| // ✅ 修改:初始化时加载数据 | |||
| // 修改:初始化时加载数据 | |||
| useEffect(() => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| console.log("✅ Session loaded, initializing job order..."); | |||
| console.log(" Session loaded, initializing job order..."); | |||
| initializationRef.current = true; | |||
| // 加载 Job Order 数据 | |||
| @@ -610,7 +610,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); | |||
| // ✅ Add event listener for manual assignment | |||
| // Add event listener for manual assignment | |||
| useEffect(() => { | |||
| const handlePickOrderAssigned = () => { | |||
| console.log("🔄 Pick order assigned event received, refreshing data..."); | |||
| @@ -624,11 +624,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }; | |||
| }, [fetchJobOrderData]); | |||
| // ✅ Handle QR code submission for matched lot (external scanning) | |||
| // Handle QR code submission for matched lot (external scanning) | |||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||
| console.log(`✅ Processing Second QR Code for lot: ${lotNo}`); | |||
| console.log(` Processing Second QR Code for lot: ${lotNo}`); | |||
| // ✅ Check if this lot was already processed recently | |||
| // Check if this lot was already processed recently | |||
| const lotKey = `${lotNo}_${Date.now()}`; | |||
| if (processedQrCodes.has(lotNo)) { | |||
| console.log(`⏭️ Lot ${lotNo} already processed, skipping...`); | |||
| @@ -652,26 +652,26 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| let successCount = 0; | |||
| for (const matchingLot of matchingLots) { | |||
| // ✅ Check if this specific item was already processed | |||
| // Check if this specific item was already processed | |||
| const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`; | |||
| if (processedQrCodes.has(itemKey)) { | |||
| console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`); | |||
| continue; | |||
| } | |||
| // ✅ Use the new second scan API | |||
| // Use the new second scan API | |||
| const result = await updateSecondQrScanStatus( | |||
| matchingLot.pickOrderId, | |||
| matchingLot.itemId, | |||
| currentUserId || 0, | |||
| matchingLot.requiredQty || 1 // ✅ 传递实际的 required quantity | |||
| matchingLot.requiredQty || 1 // 传递实际的 required quantity | |||
| ); | |||
| if (result.code === "SUCCESS") { | |||
| successCount++; | |||
| // ✅ Mark this item as processed | |||
| // Mark this item as processed | |||
| setProcessedQrCodes(prev => new Set(prev).add(itemKey)); | |||
| console.log(`✅ Second QR scan status updated for item ${matchingLot.itemId}`); | |||
| console.log(` Second QR scan status updated for item ${matchingLot.itemId}`); | |||
| } else { | |||
| console.error(`❌ Failed to update second QR scan status: ${result.message}`); | |||
| } | |||
| @@ -681,11 +681,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setQrScanSuccess(true); | |||
| setQrScanError(false); | |||
| // ✅ Set refreshing flag briefly to prevent duplicate processing | |||
| // Set refreshing flag briefly to prevent duplicate processing | |||
| setIsRefreshingData(true); | |||
| await fetchJobOrderData(); // Refresh data | |||
| // ✅ Clear refresh flag and success message after a short delay | |||
| // Clear refresh flag and success message after a short delay | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| setIsRefreshingData(false); | |||
| @@ -703,20 +703,20 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| useEffect(() => { | |||
| // ✅ Add isManualScanning and isRefreshingData checks | |||
| // Add isManualScanning and isRefreshingData checks | |||
| if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { | |||
| return; | |||
| } | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| // ✅ Check if this QR was already processed recently | |||
| // Check if this QR was already processed recently | |||
| if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { | |||
| console.log("⏭️ QR code already processed, skipping..."); | |||
| return; | |||
| } | |||
| // ✅ Mark as processed | |||
| // Mark as processed | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| setLastProcessedQr(latestQr); | |||
| @@ -752,7 +752,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]); | |||
| // ✅ ADD THIS: Cleanup effect | |||
| // ADD THIS: Cleanup effect | |||
| useEffect(() => { | |||
| return () => { | |||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||
| @@ -769,10 +769,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [qrScanInput, handleQrCodeSubmit]); | |||
| // ✅ Handle QR code submission from modal (internal scanning) | |||
| // 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}`); | |||
| console.log(` QR Code verified for lot: ${lotNo}`); | |||
| const requiredQty = selectedLotForQr.requiredQty; | |||
| const lotId = selectedLotForQr.lotId; | |||
| @@ -800,7 +800,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| ...prev, | |||
| [lotKey]: requiredQty | |||
| })); | |||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| }, 500); | |||
| // Refresh data | |||
| @@ -811,7 +811,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [selectedLotForQr, fetchJobOrderData]); | |||
| // ✅ Outside QR scanning - process QR codes from outside the page automatically | |||
| // Outside QR scanning - process QR codes from outside the page automatically | |||
| const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { | |||
| @@ -847,7 +847,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { | |||
| try { | |||
| // ✅ Use the new second scan submit API | |||
| // Use the new second scan submit API | |||
| const result = await submitSecondScanQuantity( | |||
| lot.pickOrderId, | |||
| lot.itemId, | |||
| @@ -855,13 +855,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| qty: submitQty, | |||
| isMissing: false, | |||
| isBad: false, | |||
| reason: undefined // ✅ Fix TypeScript error | |||
| reason: undefined // Fix TypeScript error | |||
| } | |||
| ); | |||
| if (result.code === "SUCCESS") { | |||
| console.log(`✅ Second scan quantity submitted: ${submitQty}`); | |||
| console.log(` Second scan quantity submitted: ${submitQty}`); | |||
| await fetchJobOrderData(); // Refresh data | |||
| } else { | |||
| console.error(`❌ Failed to submit second scan quantity: ${result.message}`); | |||
| @@ -870,13 +870,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.error("Error submitting second scan quantity:", error); | |||
| } | |||
| }, [fetchJobOrderData]); | |||
| // ✅ Handle reject lot | |||
| // Handle reject lot | |||
| // ✅ Handle pick execution form | |||
| // Handle pick execution form | |||
| const handlePickExecutionForm = useCallback((lot: any) => { | |||
| console.log("=== Pick Execution Form ==="); | |||
| console.log("Lot data:", lot); | |||
| console.log("lot.pickOrderCode:", lot.pickOrderCode); // ✅ 添加 | |||
| console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加 | |||
| console.log("lot.pickOrderId:", lot.pickOrderId); | |||
| if (!lot) { | |||
| @@ -904,8 +904,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| selectedLotForExecutionForm.itemId, | |||
| { | |||
| qty: data.actualPickQty, // verified qty | |||
| missQty: data.missQty || 0, // ✅ 添加:实际的 miss qty | |||
| badItemQty: data.badItemQty || 0, // ✅ 添加:实际的 bad item qty | |||
| missQty: data.missQty || 0, // 添加:实际的 miss qty | |||
| badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty | |||
| isMissing: data.missQty > 0, | |||
| isBad: data.badItemQty > 0, | |||
| reason: data.issueRemark || '', | |||
| @@ -916,7 +916,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Pick execution issue recorded:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ Pick execution issue recorded successfully"); | |||
| console.log(" Pick execution issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| @@ -930,7 +930,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]); | |||
| // ✅ Calculate remaining required quantity | |||
| // Calculate remaining required quantity | |||
| const calculateRemainingRequiredQty = useCallback((lot: any) => { | |||
| const requiredQty = lot.requiredQty || 0; | |||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||
| @@ -1011,7 +1011,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // Pagination data with sorting by routerIndex | |||
| const paginatedData = useMemo(() => { | |||
| // ✅ Sort by routerIndex first, then by other criteria | |||
| // Sort by routerIndex first, then by other criteria | |||
| const sortedData = [...combinedLotData].sort((a, b) => { | |||
| const aIndex = a.routerIndex || 0; | |||
| const bIndex = b.routerIndex || 0; | |||
| @@ -1035,7 +1035,7 @@ const paginatedData = useMemo(() => { | |||
| return sortedData.slice(startIndex, endIndex); | |||
| }, [combinedLotData, paginationController]); | |||
| // ✅ Add these functions for manual scanning | |||
| // Add these functions for manual scanning | |||
| const handleStartScan = useCallback(() => { | |||
| console.log(" Starting manual QR scan..."); | |||
| setIsManualScanning(true); | |||
| @@ -1061,7 +1061,7 @@ const paginatedData = useMemo(() => { | |||
| } | |||
| }, [combinedLotData.length, isManualScanning, handleStopScan]); | |||
| // ✅ Cleanup effect | |||
| // Cleanup effect | |||
| useEffect(() => { | |||
| return () => { | |||
| // Cleanup when component unmounts (e.g., when switching tabs) | |||
| @@ -1151,7 +1151,7 @@ const paginatedData = useMemo(() => { | |||
| </Button> | |||
| )} | |||
| {/* ✅ ADD THIS: Submit All Scanned Button */} | |||
| {/* ADD THIS: Submit All Scanned Button */} | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| @@ -1259,12 +1259,12 @@ const paginatedData = useMemo(() => { | |||
| height: '100%' | |||
| }}> | |||
| <Checkbox | |||
| checked={true} // ✅ 改为 true | |||
| checked={true} // 改为 true | |||
| disabled={true} | |||
| readOnly={true} | |||
| size="large" | |||
| sx={{ | |||
| color: 'success.main', // ✅ 固定为绿色 | |||
| color: 'success.main', // 固定为绿色 | |||
| '&.Mui-checked': { | |||
| color: 'success.main', | |||
| }, | |||
| @@ -1296,7 +1296,7 @@ const paginatedData = useMemo(() => { | |||
| handleSubmitPickQtyWithQty(lot, submitQty); | |||
| }} | |||
| disabled={ | |||
| // ✅ 修复:只有扫描过但未完成的才能提交 | |||
| // 修复:只有扫描过但未完成的才能提交 | |||
| lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能提交 | |||
| lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| @@ -1317,7 +1317,7 @@ const paginatedData = useMemo(() => { | |||
| size="small" | |||
| onClick={() => handlePickExecutionForm(lot)} | |||
| disabled={ | |||
| // ✅ 修复:只有扫描过但未完成的才能报告问题 | |||
| // 修复:只有扫描过但未完成的才能报告问题 | |||
| lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能报告问题 | |||
| lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| @@ -1361,7 +1361,7 @@ const paginatedData = useMemo(() => { | |||
| </Box> | |||
| </Stack> | |||
| {/* ✅ QR Code Modal */} | |||
| {/* QR Code Modal */} | |||
| <QrCodeModal | |||
| open={qrModalOpen} | |||
| onClose={() => { | |||
| @@ -1375,7 +1375,7 @@ const paginatedData = useMemo(() => { | |||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | |||
| /> | |||
| {/* ✅ Pick Execution Form Modal */} | |||
| {/* Pick Execution Form Modal */} | |||
| {pickExecutionFormOpen && selectedLotForExecutionForm && ( | |||
| <GoodPickExecutionForm | |||
| open={pickExecutionFormOpen} | |||
| @@ -1391,13 +1391,13 @@ const paginatedData = useMemo(() => { | |||
| itemCode: selectedLotForExecutionForm.itemCode, | |||
| itemName: selectedLotForExecutionForm.itemName, | |||
| pickOrderCode: selectedLotForExecutionForm.pickOrderCode, | |||
| // ✅ Add missing required properties from GetPickOrderLineInfo interface | |||
| // 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 | |||
| pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty | |||
| suggestedList: [] // Add required suggestedList property | |||
| }} | |||
| pickOrderId={selectedLotForExecutionForm.pickOrderId} | |||
| pickOrderCreateDate={new Date()} | |||
| @@ -54,7 +54,7 @@ interface PickExecutionFormProps { | |||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||
| pickOrderId?: number; | |||
| pickOrderCreateDate: any; | |||
| // ✅ Remove these props since we're not handling normal cases | |||
| // Remove these props since we're not handling normal cases | |||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||
| // selectedRowId?: number | null; | |||
| } | |||
| @@ -76,7 +76,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| selectedPickOrderLine, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| // ✅ Remove these props | |||
| // Remove these props | |||
| // onNormalPickSubmit, | |||
| // selectedRowId, | |||
| }) => { | |||
| @@ -93,7 +93,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| return Math.max(0, remainingQty); | |||
| }, []); | |||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| // ✅ Use the original required quantity, not subtracting actualPickQty | |||
| // Use the original required quantity, not subtracting actualPickQty | |||
| // The actualPickQty in the form should be independent of the database value | |||
| return lot.requiredQty || 0; | |||
| }, []); | |||
| @@ -128,7 +128,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| } | |||
| }; | |||
| // ✅ Initialize verified quantity to the received quantity (actualPickQty) | |||
| // Initialize verified quantity to the received quantity (actualPickQty) | |||
| const initialVerifiedQty = selectedLot.actualPickQty || 0; | |||
| setVerifiedQty(initialVerifiedQty); | |||
| @@ -157,14 +157,14 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| handledBy: undefined, | |||
| }); | |||
| } | |||
| // ✅ 修复:只在 open 状态改变时重新初始化,移除其他依赖 | |||
| // 修复:只在 open 状态改变时重新初始化,移除其他依赖 | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, [open]); | |||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | |||
| setFormData(prev => ({ ...prev, [field]: value })); | |||
| // ✅ Update verified quantity state when actualPickQty changes | |||
| // Update verified quantity state when actualPickQty changes | |||
| if (field === 'actualPickQty') { | |||
| setVerifiedQty(value); | |||
| } | |||
| @@ -175,11 +175,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| } | |||
| }, [errors]); | |||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| // Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| // ✅ 使用原始的接收数量,而不是 formData 中的 | |||
| // 使用原始的接收数量,而不是 formData 中的 | |||
| const receivedQty = selectedLot?.actualPickQty || 0; | |||
| const requiredQty = selectedLot?.requiredQty || 0; | |||
| const badItemQty = formData.badItemQty || 0; | |||
| @@ -189,18 +189,18 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| newErrors.actualPickQty = t('Qty is required'); | |||
| } | |||
| // ✅ 验证数量不能超过原始接收数量 | |||
| // 验证数量不能超过原始接收数量 | |||
| if (verifiedQty > receivedQty) { | |||
| newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity'); | |||
| } | |||
| // ✅ 只检查总和是否等于需求数量 | |||
| // 只检查总和是否等于需求数量 | |||
| const totalQty = verifiedQty + badItemQty + missQty; | |||
| if (totalQty !== requiredQty) { | |||
| newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); | |||
| } | |||
| // ✅ Require either missQty > 0 OR badItemQty > 0 | |||
| // Require either missQty > 0 OR badItemQty > 0 | |||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||
| @@ -219,7 +219,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| setLoading(true); | |||
| try { | |||
| // ✅ Use the verified quantity in the submission | |||
| // Use the verified quantity in the submission | |||
| const submissionData = { | |||
| ...formData, | |||
| actualPickQty: verifiedQty, | |||
| @@ -257,11 +257,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| return ( | |||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||
| <DialogTitle> | |||
| {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} | |||
| {t('Pick Execution Issue Form')} {/* Always show issue form title */} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mt: 2 }}> | |||
| {/* ✅ Add instruction text */} | |||
| {/* Add instruction text */} | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}> | |||
| @@ -271,7 +271,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| </Box> | |||
| </Grid> | |||
| {/* ✅ Keep the existing form fields */} | |||
| {/* Keep the existing form fields */} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| @@ -306,7 +306,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| //handleInputChange('actualPickQty', newValue); | |||
| }} | |||
| error={!!errors.actualPickQty} | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // ✅ 使用原始接收数量 | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量 | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| @@ -320,7 +320,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| onChange={(e) => { | |||
| const newMissQty = parseFloat(e.target.value) || 0; | |||
| handleInputChange('missQty', newMissQty); | |||
| // ✅ 不要自动修改其他字段 | |||
| // 不要自动修改其他字段 | |||
| }} | |||
| error={!!errors.missQty} | |||
| helperText={errors.missQty} | |||
| @@ -337,7 +337,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| onChange={(e) => { | |||
| const newBadItemQty = parseFloat(e.target.value) || 0; | |||
| handleInputChange('badItemQty', newBadItemQty); | |||
| // ✅ 不要自动修改其他字段 | |||
| // 不要自动修改其他字段 | |||
| }} | |||
| error={!!errors.badItemQty} | |||
| helperText={errors.badItemQty} | |||
| @@ -345,7 +345,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {/* Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| @@ -358,7 +358,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| value={formData.issueRemark || ''} | |||
| onChange={(e) => { | |||
| handleInputChange('issueRemark', e.target.value); | |||
| // ✅ Don't reset badItemQty when typing in issue remark | |||
| // Don't reset badItemQty when typing in issue remark | |||
| }} | |||
| error={!!errors.issueRemark} | |||
| helperText={errors.issueRemark} | |||
| @@ -374,7 +374,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| value={formData.handledBy ? formData.handledBy.toString() : ''} | |||
| onChange={(e) => { | |||
| handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined); | |||
| // ✅ Don't reset badItemQty when selecting handler | |||
| // Don't reset badItemQty when selecting handler | |||
| }} | |||
| label={t('handler')} | |||
| > | |||
| @@ -106,7 +106,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||
| const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail; | |||
| // ✅ 修复:根据标签页和事件来源决定是否更新打印按钮状态 | |||
| // 修复:根据标签页和事件来源决定是否更新打印按钮状态 | |||
| if (eventTabIndex === undefined || eventTabIndex === tabIndex) { | |||
| setPrintButtonsEnabled(allLotsCompleted); | |||
| console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted); | |||
| @@ -118,9 +118,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| return () => { | |||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| }; | |||
| }, [tabIndex]); // ✅ 添加 tabIndex 依赖 | |||
| }, [tabIndex]); // 添加 tabIndex 依赖 | |||
| // ✅ 新增:处理标签页切换时的打印按钮状态重置 | |||
| // 新增:处理标签页切换时的打印按钮状态重置 | |||
| useEffect(() => { | |||
| // 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态 | |||
| if (tabIndex === 2) { | |||
| @@ -141,14 +141,14 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId); | |||
| console.log("Assign by store result:", res); | |||
| // ✅ Handle different response codes | |||
| // Handle different response codes | |||
| if (res.code === "SUCCESS") { | |||
| console.log("✅ Successfully assigned pick order to store", storeId); | |||
| // ✅ Trigger refresh to show newly assigned data | |||
| console.log(" Successfully assigned pick order to store", storeId); | |||
| // Trigger refresh to show newly assigned data | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "USER_BUSY") { | |||
| console.warn("⚠️ User already has pick orders in progress:", res.message); | |||
| // ✅ Show warning but still refresh to show existing orders | |||
| // Show warning but still refresh to show existing orders | |||
| alert(`Warning: ${res.message}`); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "NO_ORDERS") { | |||
| @@ -165,7 +165,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| setIsAssigning(false); | |||
| } | |||
| }; | |||
| // ✅ Manual assignment handler - uses the action function | |||
| // Manual assignment handler - uses the action function | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| try { | |||
| @@ -189,7 +189,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| try { | |||
| const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); | |||
| if (result.message === "Successfully assigned") { | |||
| console.log("✅ Successfully assigned pick order"); | |||
| console.log(" Successfully assigned pick order"); | |||
| // 刷新数据 | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| // 重新加载未分配订单列表 | |||
| @@ -448,7 +448,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| </Stack> | |||
| </Box> | |||
| {/* Tabs section - ✅ Move the click handler here */} | |||
| {/* Tabs section - Move the click handler here */} | |||
| <Box sx={{ | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| @@ -49,7 +49,7 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ 修改:已完成的 Job Order Pick Order 接口 | |||
| // 修改:已完成的 Job Order Pick Order 接口 | |||
| interface CompletedJobOrderPickOrder { | |||
| id: number; | |||
| pickOrderId: number; | |||
| @@ -70,7 +70,7 @@ interface CompletedJobOrderPickOrder { | |||
| completedItems: number; | |||
| } | |||
| // ✅ 新增:Lot 详情接口 | |||
| // 新增:Lot 详情接口 | |||
| interface LotDetail { | |||
| lotId: number; | |||
| lotNo: string; | |||
| @@ -106,21 +106,21 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 修改:已完成 Job Order Pick Orders 状态 | |||
| // 修改:已完成 Job Order Pick Orders 状态 | |||
| const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false); | |||
| // ✅ 修改:详情视图状态 | |||
| // 修改:详情视图状态 | |||
| const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null); | |||
| const [showDetailView, setShowDetailView] = useState(false); | |||
| const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]); | |||
| const [detailLotDataLoading, setDetailLotDataLoading] = useState(false); | |||
| // ✅ 修改:搜索状态 | |||
| // 修改:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]); | |||
| // ✅ 修改:分页状态 | |||
| // 修改:分页状态 | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| @@ -129,7 +129,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的) | |||
| // 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的) | |||
| const fetchCompletedJobOrderPickOrdersData = useCallback(async () => { | |||
| if (!currentUserId) return; | |||
| @@ -139,12 +139,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId); | |||
| // ✅ Fix: Ensure the data is always an array | |||
| // Fix: Ensure the data is always an array | |||
| const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []; | |||
| setCompletedJobOrderPickOrders(safeData); | |||
| setFilteredJobOrderPickOrders(safeData); | |||
| console.log("✅ Fetched completed Job Order pick orders:", safeData); | |||
| console.log(" Fetched completed Job Order pick orders:", safeData); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching completed Job Order pick orders:", error); | |||
| setCompletedJobOrderPickOrders([]); | |||
| @@ -154,7 +154,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 新增:获取 lot 详情数据(使用新的API) | |||
| // 新增:获取 lot 详情数据(使用新的API) | |||
| const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { | |||
| setDetailLotDataLoading(true); | |||
| try { | |||
| @@ -163,7 +163,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId); | |||
| setDetailLotData(lotDetails); | |||
| console.log("✅ Fetched lot details:", lotDetails); | |||
| console.log(" Fetched lot details:", lotDetails); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching lot details:", error); | |||
| setDetailLotData([]); | |||
| @@ -172,19 +172,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, []); | |||
| // ✅ 修改:初始化时获取数据 | |||
| // 修改:初始化时获取数据 | |||
| useEffect(() => { | |||
| if (currentUserId) { | |||
| fetchCompletedJobOrderPickOrdersData(); | |||
| } | |||
| }, [currentUserId, fetchCompletedJobOrderPickOrdersData]); | |||
| // ✅ 修改:搜索功能 | |||
| // 修改:搜索功能 | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| // ✅ Fix: Ensure completedJobOrderPickOrders is an array before filtering | |||
| // Fix: Ensure completedJobOrderPickOrders is an array before filtering | |||
| if (!Array.isArray(completedJobOrderPickOrders)) { | |||
| setFilteredJobOrderPickOrders([]); | |||
| return; | |||
| @@ -207,14 +207,14 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Filtered Job Order pick orders count:", filtered.length); | |||
| }, [completedJobOrderPickOrders]); | |||
| // ✅ 修改:重置搜索 | |||
| // 修改:重置搜索 | |||
| const handleSearchReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| // ✅ Fix: Ensure completedJobOrderPickOrders is an array before setting | |||
| // Fix: Ensure completedJobOrderPickOrders is an array before setting | |||
| setFilteredJobOrderPickOrders(Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []); | |||
| }, [completedJobOrderPickOrders]); | |||
| // ✅ 修改:分页功能 | |||
| // 修改:分页功能 | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| ...prev, | |||
| @@ -230,9 +230,9 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| }, []); | |||
| // ✅ 修改:分页数据 | |||
| // 修改:分页数据 | |||
| const paginatedData = useMemo(() => { | |||
| // ✅ Fix: Ensure filteredJobOrderPickOrders is an array before calling slice | |||
| // Fix: Ensure filteredJobOrderPickOrders is an array before calling slice | |||
| if (!Array.isArray(filteredJobOrderPickOrders)) { | |||
| return []; | |||
| } | |||
| @@ -242,7 +242,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| return filteredJobOrderPickOrders.slice(startIndex, endIndex); | |||
| }, [filteredJobOrderPickOrders, paginationController]); | |||
| // ✅ 修改:搜索条件 | |||
| // 修改:搜索条件 | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| @@ -261,34 +261,34 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| ]; | |||
| // ✅ 修改:详情点击处理 | |||
| // 修改:详情点击处理 | |||
| const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { | |||
| setSelectedJobOrderPickOrder(jobOrderPickOrder); | |||
| setShowDetailView(true); | |||
| // ✅ 获取 lot 详情数据(使用新的API) | |||
| // 获取 lot 详情数据(使用新的API) | |||
| await fetchLotDetailsData(jobOrderPickOrder.pickOrderId); | |||
| // ✅ 触发打印按钮状态更新 - 基于详情数据 | |||
| // 触发打印按钮状态更新 - 基于详情数据 | |||
| const allCompleted = jobOrderPickOrder.secondScanCompleted; | |||
| // ✅ 发送事件,包含标签页信息 | |||
| // 发送事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 3 // ✅ 明确指定这是来自标签页 3 的事件 | |||
| tabIndex: 3 // 明确指定这是来自标签页 3 的事件 | |||
| } | |||
| })); | |||
| }, [fetchLotDetailsData]); | |||
| // ✅ 修改:返回列表视图 | |||
| // 修改:返回列表视图 | |||
| const handleBackToList = useCallback(() => { | |||
| setShowDetailView(false); | |||
| setSelectedJobOrderPickOrder(null); | |||
| setDetailLotData([]); | |||
| // ✅ 返回列表时禁用打印按钮 | |||
| // 返回列表时禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -335,7 +335,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| },[t, selectedJobOrderPickOrder]); | |||
| // ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | |||
| // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 | |||
| if (showDetailView && selectedJobOrderPickOrder) { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| @@ -386,7 +386,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| </CardContent> | |||
| </Card> | |||
| {/* ✅ 修改:Lot 详情表格 - 添加复选框列 */} | |||
| {/* 修改:Lot 详情表格 - 添加复选框列 */} | |||
| <Card> | |||
| <CardContent> | |||
| <Typography variant="h6" gutterBottom> | |||
| @@ -446,7 +446,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| <TableCell align="right"> | |||
| {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) | |||
| </TableCell> | |||
| {/* ✅ 修改:Processing Status 使用复选框 */} | |||
| {/* 修改:Processing Status 使用复选框 */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ | |||
| display: 'flex', | |||
| @@ -473,7 +473,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| /> | |||
| </Box> | |||
| </TableCell> | |||
| {/* ✅ 修改:Second Scan Status 使用复选框 */} | |||
| {/* 修改:Second Scan Status 使用复选框 */} | |||
| <TableCell align="center"> | |||
| <Box sx={{ | |||
| display: 'flex', | |||
| @@ -514,7 +514,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| ); | |||
| } | |||
| // ✅ 修改:默认列表视图 | |||
| // 修改:默认列表视图 | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Box> | |||
| @@ -24,7 +24,7 @@ import { GetPickOrderLineInfo, recordPickExecutionIssue } from "@/app/api/pickOr | |||
| import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | |||
| import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; | |||
| import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; // ✅ Add this import | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import | |||
| import PickExecutionForm from "./PickExecutionForm"; | |||
| interface LotPickData { | |||
| id: number; | |||
| @@ -41,7 +41,7 @@ interface LotPickData { | |||
| outQty: number; | |||
| holdQty: number; | |||
| totalPickedByAllPickOrders: number; | |||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // ✅ 添加 'rejected' | |||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // 添加 'rejected' | |||
| stockOutLineId?: number; | |||
| stockOutLineStatus?: string; | |||
| stockOutLineQty?: number; | |||
| @@ -56,7 +56,7 @@ interface PickQtyData { | |||
| interface LotTableProps { | |||
| lotData: LotPickData[]; | |||
| selectedRowId: number | null; | |||
| selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // ✅ 添加 pickOrderId | |||
| selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // 添加 pickOrderId | |||
| pickQtyData: PickQtyData; | |||
| selectedLotRowId: string | null; | |||
| selectedLotId: number | null; | |||
| @@ -77,7 +77,7 @@ interface LotTableProps { | |||
| onLotDataRefresh: () => Promise<void>; | |||
| } | |||
| // ✅ QR Code Modal Component | |||
| // QR Code Modal Component | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| onClose: () => void; | |||
| @@ -88,53 +88,53 @@ const QrCodeModal: React.FC<{ | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const [manualInput, setManualInput] = useState<string>(''); | |||
| const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); | |||
| // ✅ Add state to track manual input submission | |||
| // Add state to track manual input submission | |||
| const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); | |||
| const [manualInputError, setManualInputError] = useState<boolean>(false); | |||
| const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false); | |||
| const [qrScanFailed, setQrScanFailed] = useState<boolean>(false); | |||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | |||
| // ✅ Add state to track processed QR codes to prevent re-processing | |||
| // Add state to track processed QR codes to prevent re-processing | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| // ✅ Add state to store the scanned QR result | |||
| // Add state to store the scanned QR result | |||
| const [scannedQrResult, setScannedQrResult] = useState<string>(''); | |||
| // ✅ Process scanned QR codes with new format | |||
| // Process scanned QR codes with new format | |||
| useEffect(() => { | |||
| if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| // ✅ Check if this QR code has already been processed | |||
| // Check if this QR code has already been processed | |||
| if (processedQrCodes.has(latestQr)) { | |||
| console.log("QR code already processed, skipping..."); | |||
| return; | |||
| } | |||
| // ✅ Add to processed set immediately to prevent re-processing | |||
| // Add to processed set immediately to prevent re-processing | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| try { | |||
| // ✅ Parse QR code as JSON | |||
| // Parse QR code as JSON | |||
| const qrData = JSON.parse(latestQr); | |||
| // ✅ Check if it has the expected structure | |||
| // Check if it has the expected structure | |||
| if (qrData.stockInLineId && qrData.itemId) { | |||
| setIsProcessingQr(true); | |||
| setQrScanFailed(false); | |||
| // ✅ Fetch stock in line info to get lotNo | |||
| // Fetch stock in line info to get lotNo | |||
| fetchStockInLineInfo(qrData.stockInLineId) | |||
| .then((stockInLineInfo) => { | |||
| console.log("Stock in line info:", stockInLineInfo); | |||
| // ✅ Store the scanned result for display | |||
| // Store the scanned result for display | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| // ✅ Compare lotNo from API with expected lotNo | |||
| // Compare lotNo from API with expected lotNo | |||
| if (stockInLineInfo.lotNo === lot.lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lot.lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lot.lotNo}`); | |||
| setQrScanSuccess(true); | |||
| onQrCodeSubmit(lot.lotNo); | |||
| onClose(); | |||
| @@ -144,7 +144,7 @@ const QrCodeModal: React.FC<{ | |||
| setQrScanFailed(true); | |||
| setManualInputError(true); | |||
| setManualInputSubmitted(true); | |||
| // ✅ DON'T stop scanning - allow new QR codes to be processed | |||
| // DON'T stop scanning - allow new QR codes to be processed | |||
| } | |||
| }) | |||
| .catch((error) => { | |||
| @@ -153,16 +153,16 @@ const QrCodeModal: React.FC<{ | |||
| setQrScanFailed(true); | |||
| setManualInputError(true); | |||
| setManualInputSubmitted(true); | |||
| // ✅ DON'T stop scanning - allow new QR codes to be processed | |||
| // DON'T stop scanning - allow new QR codes to be processed | |||
| }) | |||
| .finally(() => { | |||
| setIsProcessingQr(false); | |||
| }); | |||
| } else { | |||
| // ✅ Fallback to old format (direct lotNo comparison) | |||
| // Fallback to old format (direct lotNo comparison) | |||
| const qrContent = latestQr.replace(/[{}]/g, ''); | |||
| // ✅ Store the scanned result for display | |||
| // Store the scanned result for display | |||
| setScannedQrResult(qrContent); | |||
| if (qrContent === lot.lotNo) { | |||
| @@ -174,15 +174,15 @@ const QrCodeModal: React.FC<{ | |||
| setQrScanFailed(true); | |||
| setManualInputError(true); | |||
| setManualInputSubmitted(true); | |||
| // ✅ DON'T stop scanning - allow new QR codes to be processed | |||
| // DON'T stop scanning - allow new QR codes to be processed | |||
| } | |||
| } | |||
| } catch (error) { | |||
| // ✅ If JSON parsing fails, fallback to old format | |||
| // If JSON parsing fails, fallback to old format | |||
| console.log("QR code is not JSON format, trying direct comparison"); | |||
| const qrContent = latestQr.replace(/[{}]/g, ''); | |||
| // ✅ Store the scanned result for display | |||
| // Store the scanned result for display | |||
| setScannedQrResult(qrContent); | |||
| if (qrContent === lot.lotNo) { | |||
| @@ -194,13 +194,13 @@ const QrCodeModal: React.FC<{ | |||
| setQrScanFailed(true); | |||
| setManualInputError(true); | |||
| setManualInputSubmitted(true); | |||
| // ✅ DON'T stop scanning - allow new QR codes to be processed | |||
| // DON'T stop scanning - allow new QR codes to be processed | |||
| } | |||
| } | |||
| } | |||
| }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, stopScan]); | |||
| // ✅ Clear states when modal opens or lot changes | |||
| // Clear states when modal opens or lot changes | |||
| useEffect(() => { | |||
| if (open) { | |||
| setManualInput(''); | |||
| @@ -209,8 +209,8 @@ const QrCodeModal: React.FC<{ | |||
| setIsProcessingQr(false); | |||
| setQrScanFailed(false); | |||
| setQrScanSuccess(false); | |||
| setScannedQrResult(''); // ✅ Clear scanned result | |||
| // ✅ Clear processed QR codes when modal opens | |||
| setScannedQrResult(''); // Clear scanned result | |||
| // Clear processed QR codes when modal opens | |||
| setProcessedQrCodes(new Set()); | |||
| } | |||
| }, [open]); | |||
| @@ -223,13 +223,13 @@ const QrCodeModal: React.FC<{ | |||
| setIsProcessingQr(false); | |||
| setQrScanFailed(false); | |||
| setQrScanSuccess(false); | |||
| setScannedQrResult(''); // ✅ Clear scanned result | |||
| // ✅ Clear processed QR codes when lot changes | |||
| setScannedQrResult(''); // Clear scanned result | |||
| // Clear processed QR codes when lot changes | |||
| setProcessedQrCodes(new Set()); | |||
| } | |||
| }, [lot]); | |||
| // ✅ Auto-submit manual input when it matches (but only if QR scan hasn't failed) | |||
| // Auto-submit manual input when it matches (but only if QR scan hasn't failed) | |||
| useEffect(() => { | |||
| if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) { | |||
| console.log('🔄 Auto-submitting manual input:', manualInput.trim()); | |||
| @@ -247,7 +247,7 @@ const QrCodeModal: React.FC<{ | |||
| } | |||
| }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]); | |||
| // ✅ Add the missing handleManualSubmit function | |||
| // Add the missing handleManualSubmit function | |||
| const handleManualSubmit = () => { | |||
| if (manualInput.trim() === lot?.lotNo) { | |||
| setQrScanSuccess(true); | |||
| @@ -261,7 +261,7 @@ const QrCodeModal: React.FC<{ | |||
| } | |||
| }; | |||
| // ✅ Add function to restart scanning after manual input error | |||
| // Add function to restart scanning after manual input error | |||
| const handleRestartScan = () => { | |||
| setQrScanFailed(false); | |||
| setManualInputError(false); | |||
| @@ -292,7 +292,7 @@ const QrCodeModal: React.FC<{ | |||
| {t("QR Code Scan for Lot")}: {lot?.lotNo} | |||
| </Typography> | |||
| {/* ✅ Show processing status */} | |||
| {/* Show processing status */} | |||
| {isProcessingQr && ( | |||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}> | |||
| <Typography variant="body2" color="primary"> | |||
| @@ -312,7 +312,7 @@ const QrCodeModal: React.FC<{ | |||
| value={manualInput} | |||
| onChange={(e) => { | |||
| setManualInput(e.target.value); | |||
| // ✅ Reset error states when user starts typing | |||
| // Reset error states when user starts typing | |||
| if (qrScanFailed || manualInputError) { | |||
| setQrScanFailed(false); | |||
| setManualInputError(false); | |||
| @@ -352,7 +352,7 @@ const QrCodeModal: React.FC<{ | |||
| {qrScanSuccess && ( | |||
| <Typography variant="caption" color="success" display="block"> | |||
| ✅ {t("Verified successfully!")} | |||
| {t("Verified successfully!")} | |||
| </Typography> | |||
| )} | |||
| </Box> | |||
| @@ -395,10 +395,10 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| const stockOutLineQty = lot.stockOutLineQty || 0; | |||
| return Math.max(0, requiredQty - stockOutLineQty); | |||
| }, []); | |||
| // ✅ Add QR scanner context | |||
| // Add QR scanner context | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({}); | |||
| // ✅ Add state for QR input modal | |||
| // Add state for QR input modal | |||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||
| const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null); | |||
| const [manualQrInput, setManualQrInput] = useState<string>(''); | |||
| @@ -409,7 +409,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| pageSize: 10, | |||
| }); | |||
| // ✅ 添加状态消息生成函数 | |||
| // 添加状态消息生成函数 | |||
| const getStatusMessage = useCallback((lot: LotPickData) => { | |||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | |||
| @@ -483,45 +483,45 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| return null; | |||
| }, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]); | |||
| // ✅ Handle QR code submission | |||
| // Handle QR code submission | |||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||
| if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | |||
| console.log(`✅ QR Code verified for lot: ${lotNo}`); | |||
| console.log(` QR Code verified for lot: ${lotNo}`); | |||
| if (!selectedLotForQr.stockOutLineId) { | |||
| console.error("No stock out line ID found for this lot"); | |||
| alert("No stock out line found for this lot. Please contact administrator."); | |||
| return; | |||
| } | |||
| // ✅ Store the required quantity before creating stock out line | |||
| // Store the required quantity before creating stock out line | |||
| const requiredQty = selectedLotForQr.requiredQty; | |||
| const lotId = selectedLotForQr.lotId; | |||
| try { | |||
| // ✅ Update stock out line status to 'checked' (QR scan completed) | |||
| // Update stock out line status to 'checked' (QR scan completed) | |||
| const stockOutLineUpdate = await updateStockOutLineStatus({ | |||
| id: selectedLotForQr.stockOutLineId, | |||
| status: 'checked', | |||
| qty: selectedLotForQr.stockOutLineQty || 0 | |||
| }); | |||
| console.log("✅ Stock out line updated to 'checked':", stockOutLineUpdate); | |||
| console.log(" Stock out line updated to 'checked':", stockOutLineUpdate); | |||
| // ✅ Close modal | |||
| // Close modal | |||
| setQrModalOpen(false); | |||
| setSelectedLotForQr(null); | |||
| if (onLotDataRefresh) { | |||
| await onLotDataRefresh(); | |||
| } | |||
| // ✅ Set pick quantity AFTER stock out line update is complete | |||
| // Set pick quantity AFTER stock out line update is complete | |||
| if (selectedRowId) { | |||
| // Add a small delay to ensure the data refresh is complete | |||
| setTimeout(() => { | |||
| onPickQtyChange(selectedRowId, lotId, requiredQty); | |||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||
| }, 500); // 500ms delay to ensure refresh is complete | |||
| } | |||
| // ✅ Show success message | |||
| // Show success message | |||
| console.log("Stock out line updated successfully!"); | |||
| } catch (error) { | |||
| @@ -529,17 +529,17 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| alert("Failed to update lot status. Please try again."); | |||
| } | |||
| } else { | |||
| // ✅ Handle case where lot numbers don't match | |||
| // Handle case where lot numbers don't match | |||
| console.error("QR scan mismatch:", { scanned: lotNo, expected: selectedLotForQr?.lotNo }); | |||
| alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`); | |||
| } | |||
| }, [selectedLotForQr, selectedRowId, onPickQtyChange]); | |||
| // ✅ 添加 PickExecutionForm 相关的状态 | |||
| // 添加 PickExecutionForm 相关的状态 | |||
| const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false); | |||
| const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null); | |||
| // ✅ 添加处理函数 | |||
| // 添加处理函数 | |||
| const handlePickExecutionForm = useCallback((lot: LotPickData) => { | |||
| console.log("=== Pick Execution Form ==="); | |||
| console.log("Lot data:", lot); | |||
| @@ -561,12 +561,12 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| try { | |||
| console.log("Pick execution form submitted:", data); | |||
| // ✅ 调用 API 提交数据 | |||
| // 调用 API 提交数据 | |||
| const result = await recordPickExecutionIssue(data); | |||
| console.log("Pick execution issue recorded:", result); | |||
| if (result && result.code === "SUCCESS") { | |||
| console.log("✅ Pick execution issue recorded successfully"); | |||
| console.log(" Pick execution issue recorded successfully"); | |||
| } else { | |||
| console.error("❌ Failed to record pick execution issue:", result); | |||
| } | |||
| @@ -574,7 +574,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| setPickExecutionFormOpen(false); | |||
| setSelectedLotForExecutionForm(null); | |||
| // ✅ 刷新数据 | |||
| // 刷新数据 | |||
| if (onDataRefresh) { | |||
| await onDataRefresh(); | |||
| } | |||
| @@ -636,10 +636,10 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| <Checkbox | |||
| checked={selectedLotRowId === `row_${index}`} | |||
| onChange={() => onLotSelection(`row_${index}`, lot.lotId)} | |||
| // ✅ 禁用 rejected、expired 和 status_unavailable 的批次 | |||
| // 禁用 rejected、expired 和 status_unavailable 的批次 | |||
| disabled={lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected'} // ✅ 添加 rejected | |||
| lot.lotAvailability === 'rejected'} // 添加 rejected | |||
| value={`row_${index}`} | |||
| name="lot-selection" | |||
| /> | |||
| @@ -659,7 +659,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| <Typography variant="caption" color="error" display="block"> | |||
| ({lot.lotAvailability === 'expired' ? 'Expired' : | |||
| lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : | |||
| lot.lotAvailability === 'rejected' ? 'Rejected' : // ✅ 添加 rejected 显示 | |||
| lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示 | |||
| 'Unavailable'}) | |||
| </Typography> | |||
| )} */} | |||
| @@ -718,13 +718,13 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| direction="row" | |||
| spacing={1} | |||
| alignItems="center" | |||
| justifyContent="center" // ✅ 添加水平居中 | |||
| justifyContent="center" // 添加水平居中 | |||
| sx={{ | |||
| width: '100%', // ✅ 确保占满整个单元格宽度 | |||
| minHeight: '40px' // ✅ 设置最小高度确保垂直居中 | |||
| width: '100%', // 确保占满整个单元格宽度 | |||
| minHeight: '40px' // 设置最小高度确保垂直居中 | |||
| }} | |||
| > | |||
| {/* ✅ 恢复 TextField 用于正常数量输入 */} | |||
| {/* 恢复 TextField 用于正常数量输入 */} | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| @@ -763,7 +763,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| placeholder="0" | |||
| /> | |||
| {/* ✅ 添加 Pick Form 按钮用于问题情况 */} | |||
| {/* 添加 Pick Form 按钮用于问题情况 */} | |||
| <Button | |||
| variant="outlined" | |||
| size="small" | |||
| @@ -806,12 +806,12 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| disabled={ | |||
| (lot.lotAvailability === 'expired' || | |||
| lot.lotAvailability === 'status_unavailable' || | |||
| lot.lotAvailability === 'rejected') || // ✅ 添加 rejected | |||
| lot.lotAvailability === 'rejected') || // 添加 rejected | |||
| !pickQtyData[selectedRowId!]?.[lot.lotId] || | |||
| !lot.stockOutLineStatus || | |||
| !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) | |||
| } | |||
| // ✅ Allow submission for available AND insufficient_stock lots | |||
| // Allow submission for available AND insufficient_stock lots | |||
| sx={{ | |||
| fontSize: '0.75rem', | |||
| py: 0.5, | |||
| @@ -829,7 +829,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| </Table> | |||
| </TableContainer> | |||
| {/* ✅ Status Messages Display */} | |||
| {/* Status Messages Display */} | |||
| {paginatedLotTableData.length > 0 && ( | |||
| <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}> | |||
| {paginatedLotTableData.map((lot, index) => ( | |||
| @@ -858,7 +858,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| } | |||
| /> | |||
| {/* ✅ QR Code Modal */} | |||
| {/* QR Code Modal */} | |||
| <QrCodeModal | |||
| open={qrModalOpen} | |||
| onClose={() => { | |||
| @@ -871,7 +871,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| onQrCodeSubmit={handleQrCodeSubmit} | |||
| /> | |||
| {/* ✅ Pick Execution Form Modal */} | |||
| {/* Pick Execution Form Modal */} | |||
| {pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && ( | |||
| <PickExecutionForm | |||
| open={pickExecutionFormOpen} | |||
| @@ -69,7 +69,7 @@ import SearchBox, { Criterion } from "../SearchBox"; | |||
| import dayjs from "dayjs"; | |||
| import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; | |||
| import LotTable from './LotTable'; | |||
| import PickOrderDetailsTable from './PickOrderDetailsTable'; // ✅ Import the new component | |||
| import PickOrderDetailsTable from './PickOrderDetailsTable'; // Import the new component | |||
| import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| @@ -147,7 +147,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null); | |||
| const [selectedLotId, setSelectedLotId] = useState<number | null>(null); | |||
| // ✅ Keep only the main table paging controller | |||
| // Keep only the main table paging controller | |||
| const [mainTablePagingController, setMainTablePagingController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| @@ -383,14 +383,14 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| try { | |||
| // ✅ FIXED: 计算累计拣货数量 | |||
| // FIXED: 计算累计拣货数量 | |||
| const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | |||
| console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0); | |||
| console.log("🔍 DEBUG - Current submit:", qty); | |||
| console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot); | |||
| console.log("�� DEBUG - Required qty:", selectedLot.requiredQty); | |||
| // ✅ FIXED: 状态应该基于累计拣货数量 | |||
| // FIXED: 状态应该基于累计拣货数量 | |||
| let newStatus = 'partially_completed'; | |||
| if (totalPickedForThisLot >= selectedLot.requiredQty) { | |||
| newStatus = 'completed'; | |||
| @@ -405,7 +405,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| qty: qty | |||
| }); | |||
| console.log("✅ Stock out line updated:", stockOutLineUpdate); | |||
| console.log(" Stock out line updated:", stockOutLineUpdate); | |||
| } catch (error) { | |||
| console.error("❌ Error updating stock out line:", error); | |||
| @@ -423,11 +423,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Inventory lot line updated:", inventoryLotLineUpdate); | |||
| } | |||
| // ✅ RE-ENABLE: Check if pick order should be completed | |||
| // RE-ENABLE: Check if pick order should be completed | |||
| if (newStatus === 'completed') { | |||
| console.log("✅ Stock out line completed, checking if entire pick order is complete..."); | |||
| console.log(" Stock out line completed, checking if entire pick order is complete..."); | |||
| // ✅ 添加调试日志来查看所有 pick orders 的 consoCode | |||
| // 添加调试日志来查看所有 pick orders 的 consoCode | |||
| console.log("📋 DEBUG - All pick orders and their consoCodes:"); | |||
| if (pickOrderDetails) { | |||
| pickOrderDetails.pickOrders.forEach((pickOrder, index) => { | |||
| @@ -435,7 +435,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }); | |||
| } | |||
| // ✅ FIXED: 直接查找 consoCode,不依赖 selectedRow | |||
| // FIXED: 直接查找 consoCode,不依赖 selectedRow | |||
| if (pickOrderDetails) { | |||
| let currentConsoCode: string | null = null; | |||
| @@ -443,7 +443,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| for (const pickOrder of pickOrderDetails.pickOrders) { | |||
| const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); | |||
| if (foundLine) { | |||
| // ✅ 直接使用 pickOrder.code 作为 consoCode | |||
| // 直接使用 pickOrder.code 作为 consoCode | |||
| currentConsoCode = pickOrder.consoCode; | |||
| console.log(`�� DEBUG - Found consoCode for line ${selectedRowId}: ${currentConsoCode} (from pick order ${pickOrder.id})`); | |||
| break; | |||
| @@ -545,7 +545,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setSelectedItemForQc(item); | |||
| }, []); | |||
| // ✅ Main table pagination handlers | |||
| // Main table pagination handlers | |||
| const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setMainTablePagingController(prev => ({ | |||
| ...prev, | |||
| @@ -781,7 +781,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| } | |||
| const stockOutLineData: CreateStockOutLine = { | |||
| consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // ✅ 使用正确的 consoCode | |||
| consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // 使用正确的 consoCode | |||
| pickOrderLineId: selectedRowId, | |||
| inventoryLotLineId: inventoryLotLineId, | |||
| qty: 0.0 | |||
| @@ -806,7 +806,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| await handleFetchAllPickOrderDetails(); | |||
| console.log("✅ Data refresh completed - lot selection maintained!"); | |||
| console.log(" Data refresh completed - lot selection maintained!"); | |||
| } catch (refreshError) { | |||
| console.error("❌ Error refreshing data:", refreshError); | |||
| } | |||
| @@ -834,13 +834,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setSelectedLotRowId(currentSelectedLotRowId); | |||
| setSelectedLotId(currentSelectedLotId); | |||
| console.log("✅ Data refreshed with selection preserved"); | |||
| console.log(" Data refreshed with selection preserved"); | |||
| } catch (error) { | |||
| console.error("❌ Error refreshing data:", error); | |||
| } | |||
| }, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]); | |||
| // ✅ Search criteria | |||
| // Search criteria | |||
| const searchCriteria: Criterion<any>[] = useMemo( | |||
| () => [ | |||
| { | |||
| @@ -868,7 +868,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| [t], | |||
| ); | |||
| // ✅ Search handler | |||
| // Search handler | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| @@ -899,7 +899,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| console.log("Filtered pick orders count:", filtered.length); | |||
| }, [originalPickOrderData, t]); | |||
| // ✅ Reset handler | |||
| // Reset handler | |||
| const handleReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| if (originalPickOrderData) { | |||
| @@ -907,7 +907,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| }, [originalPickOrderData]); | |||
| // ✅ Debug the lot data | |||
| // Debug the lot data | |||
| useEffect(() => { | |||
| console.log("Lot data:", lotData); | |||
| console.log("Pick Qty Data:", pickQtyData); | |||
| @@ -925,7 +925,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| /> | |||
| </Box> | |||
| {/* ✅ Main table using the new component */} | |||
| {/* Main table using the new component */} | |||
| <Box> | |||
| <Typography variant="h6" gutterBottom> | |||
| {t("Pick Order Details")} | |||
| @@ -969,7 +969,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setShowInputBody={setShowInputBody} | |||
| selectedLotForInput={selectedLotForInput} | |||
| generateInputBody={generateInputBody} | |||
| // ✅ Add missing props | |||
| // Add missing props | |||
| totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed | |||
| outQty={0} // You can calculate this from lotData if needed | |||
| holdQty={0} // You can calculate this from lotData if needed | |||
| @@ -53,7 +53,7 @@ interface PickExecutionFormProps { | |||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||
| pickOrderId?: number; | |||
| pickOrderCreateDate: any; | |||
| // ✅ Remove these props since we're not handling normal cases | |||
| // Remove these props since we're not handling normal cases | |||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||
| // selectedRowId?: number | null; | |||
| } | |||
| @@ -75,7 +75,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| selectedPickOrderLine, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| // ✅ Remove these props | |||
| // Remove these props | |||
| // onNormalPickSubmit, | |||
| // selectedRowId, | |||
| }) => { | |||
| @@ -166,7 +166,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| } | |||
| }, [errors]); | |||
| // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| // Update form validation to require either missQty > 0 OR badItemQty > 0 | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| @@ -174,17 +174,17 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| newErrors.actualPickQty = t('Qty is required'); | |||
| } | |||
| // ✅ ADD: Check if actual pick qty exceeds remaining available qty | |||
| // ADD: Check if actual pick qty exceeds remaining available qty | |||
| if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { | |||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); | |||
| } | |||
| // ✅ ADD: Check if actual pick qty exceeds required qty | |||
| // ADD: Check if actual pick qty exceeds required qty | |||
| if (formData.actualPickQty && formData.actualPickQty > requiredQty) { | |||
| newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); | |||
| } | |||
| // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) | |||
| // NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) | |||
| const hasMissQty = formData.missQty && formData.missQty > 0; | |||
| const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; | |||
| @@ -229,11 +229,11 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| return ( | |||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||
| <DialogTitle> | |||
| {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} | |||
| {t('Pick Execution Issue Form')} {/* Always show issue form title */} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mt: 2 }}> | |||
| {/* ✅ Add instruction text */} | |||
| {/* Add instruction text */} | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}> | |||
| @@ -243,7 +243,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| </Box> | |||
| </Grid> | |||
| {/* ✅ Keep the existing form fields */} | |||
| {/* Keep the existing form fields */} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| @@ -305,7 +305,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| /> | |||
| </Grid> | |||
| {/* ✅ Show issue description and handler fields when bad items > 0 */} | |||
| {/* Show issue description and handler fields when bad items > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| @@ -49,7 +49,7 @@ const PickOrderDetailsTable: React.FC<PickOrderDetailsTableProps> = ({ | |||
| const availableQty = line.availableQty ?? 0; | |||
| const balanceToPick = availableQty - line.requiredQty; | |||
| // ✅ Handle both string and array date formats from the optimized API | |||
| // Handle both string and array date formats from the optimized API | |||
| let formattedTargetDate = 'N/A'; | |||
| if (pickOrder.targetDate) { | |||
| if (typeof pickOrder.targetDate === 'string') { | |||
| @@ -66,14 +66,14 @@ const PickOrderDetailsTable: React.FC<PickOrderDetailsTableProps> = ({ | |||
| pickOrderCode: pickOrder.code, | |||
| targetDate: formattedTargetDate, | |||
| balanceToPick: balanceToPick, | |||
| pickedQty: line.pickedQty, // ✅ This now comes from the optimized API | |||
| pickedQty: line.pickedQty, // This now comes from the optimized API | |||
| availableQty: availableQty, | |||
| }; | |||
| }) | |||
| ); | |||
| }, [pickOrderDetails]); | |||
| // ✅ Paginated data | |||
| // Paginated data | |||
| const paginatedMainTableData = useMemo(() => { | |||
| const startIndex = pageNum * pageSize; | |||
| const endIndex = startIndex + pageSize; | |||
| @@ -33,7 +33,7 @@ import EscalationComponent from "../PoDetail/EscalationComponent"; | |||
| import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; | |||
| import { | |||
| updateInventoryLotLineStatus | |||
| } from "@/app/api/inventory/actions"; // ✅ 导入新的 API | |||
| } from "@/app/api/inventory/actions"; // 导入新的 API | |||
| import { dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| @@ -42,8 +42,8 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| order?: number; // ✅ Add order property | |||
| stableId?: string; // ✅ Also add stableId for better row identification | |||
| order?: number; // Add order property | |||
| stableId?: string; // Also add stableId for better row identification | |||
| } | |||
| interface Props extends CommonProps { | |||
| itemDetail: GetPickOrderLineInfo & { | |||
| @@ -55,7 +55,7 @@ interface Props extends CommonProps { | |||
| selectedLotId?: number; | |||
| onStockOutLineUpdate?: () => void; | |||
| lotData: LotPickData[]; | |||
| // ✅ Add missing props | |||
| // Add missing props | |||
| pickQtyData?: PickQtyData; | |||
| selectedRowId?: number; | |||
| } | |||
| @@ -104,7 +104,7 @@ interface Props extends CommonProps { | |||
| }; | |||
| qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem | |||
| setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem | |||
| // ✅ Add props for stock out line update | |||
| // Add props for stock out line update | |||
| selectedLotId?: number; | |||
| onStockOutLineUpdate?: () => void; | |||
| lotData: LotPickData[]; | |||
| @@ -193,7 +193,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed | |||
| type: "pick_order_qc", | |||
| remarks: item.remarks || "", | |||
| qcPassed: item.isPassed, // ✅ This will now be included | |||
| qcPassed: item.isPassed, // This will now be included | |||
| })); | |||
| // Store the submitted data for debug display | |||
| @@ -217,17 +217,17 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| } | |||
| }; | |||
| // ✅ 修改:在组件开始时自动设置失败数量 | |||
| // 修改:在组件开始时自动设置失败数量 | |||
| useEffect(() => { | |||
| if (itemDetail && qcItems.length > 0 && selectedLotId) { | |||
| // ✅ 获取选中的批次数据 | |||
| // 获取选中的批次数据 | |||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||
| if (selectedLot) { | |||
| // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||
| // 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||
| const updatedQcItems = qcItems.map((item, index) => ({ | |||
| ...item, | |||
| failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty | |||
| // ✅ Add stable order and ID fields | |||
| // Add stable order and ID fields | |||
| order: index, | |||
| stableId: `qc-${item.id}-${index}` | |||
| })); | |||
| @@ -236,7 +236,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| } | |||
| }, [itemDetail, qcItems.length, selectedLotId, lotData]); | |||
| // ✅ Add this helper function at the top of the component | |||
| // Add this helper function at the top of the component | |||
| const safeClose = useCallback(() => { | |||
| if (onClose) { | |||
| // Create a mock event object that satisfies the Modal onClose signature | |||
| @@ -259,12 +259,12 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| isPersistent: () => false | |||
| } as any; | |||
| // ✅ Fixed: Pass both event and reason parameters | |||
| // Fixed: Pass both event and reason parameters | |||
| onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason | |||
| } | |||
| }, [onClose]); | |||
| // ✅ 修改:移除 alert 弹窗,改为控制台日志 | |||
| // 修改:移除 alert 弹窗,改为控制台日志 | |||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | |||
| async (data, event) => { | |||
| setIsSubmitting(true); | |||
| @@ -276,7 +276,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| const validationErrors : string[] = []; | |||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||
| // ✅ Add safety check for selectedLot | |||
| // Add safety check for selectedLot | |||
| if (!selectedLot) { | |||
| console.error("Selected lot not found"); | |||
| return; | |||
| @@ -313,23 +313,23 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| // ✅ Handle different QC decisions | |||
| // Handle different QC decisions | |||
| if (selectedLotId) { | |||
| try { | |||
| const allPassed = qcData.qcItems.every(item => item.isPassed); | |||
| if (qcDecision === "2") { | |||
| // ✅ QC Decision 2: Report and Re-pick | |||
| // QC Decision 2: Report and Re-pick | |||
| console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable"); | |||
| // ✅ Inventory lot line status: unavailable | |||
| // Inventory lot line status: unavailable | |||
| if (selectedLot) { | |||
| try { | |||
| console.log("=== DEBUG: Updating inventory lot line status ==="); | |||
| console.log("Selected lot:", selectedLot); | |||
| console.log("Selected lot ID:", selectedLotId); | |||
| // ✅ FIX: Only send the fields that the backend expects | |||
| // FIX: Only send the fields that the backend expects | |||
| const updateData = { | |||
| inventoryLotLineId: selectedLot.lotId, | |||
| status: 'unavailable' | |||
| @@ -339,7 +339,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| console.log("Update data:", updateData); | |||
| const result = await updateInventoryLotLineStatus(updateData); | |||
| console.log("✅ Inventory lot line status updated successfully:", result); | |||
| console.log(" Inventory lot line status updated successfully:", result); | |||
| } catch (error) { | |||
| console.error("❌ Error updating inventory lot line status:", error); | |||
| @@ -359,28 +359,28 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| return; | |||
| } | |||
| // ✅ Close modal and refresh data | |||
| safeClose(); // ✅ Fixed: Use safe close function with both parameters | |||
| // Close modal and refresh data | |||
| safeClose(); // Fixed: Use safe close function with both parameters | |||
| if (onStockOutLineUpdate) { | |||
| onStockOutLineUpdate(); | |||
| } | |||
| } else if (qcDecision === "1") { | |||
| // ✅ QC Decision 1: Accept | |||
| // QC Decision 1: Accept | |||
| console.log("QC Decision 1 - Accept: QC passed"); | |||
| // ✅ Stock out line status: checked (QC completed) | |||
| // Stock out line status: checked (QC completed) | |||
| await updateStockOutLineStatus({ | |||
| id: selectedLotId, | |||
| status: 'checked', | |||
| qty: acceptQty || 0 | |||
| }); | |||
| // ✅ Inventory lot line status: NO CHANGE needed | |||
| // Inventory lot line status: NO CHANGE needed | |||
| // Keep the existing status from handleSubmitPickQty | |||
| // ✅ Close modal and refresh data | |||
| safeClose(); // ✅ Fixed: Use safe close function with both parameters | |||
| // Close modal and refresh data | |||
| safeClose(); // Fixed: Use safe close function with both parameters | |||
| if (onStockOutLineUpdate) { | |||
| onStockOutLineUpdate(); | |||
| } | |||
| @@ -399,7 +399,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| console.log("QC results saved successfully!"); | |||
| // ✅ Show warning dialog for failed QC items when accepting | |||
| // Show warning dialog for failed QC items when accepting | |||
| if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { | |||
| submitDialogWithWarning(() => { | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| @@ -448,7 +448,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = e.target.value === "true"; | |||
| // ✅ Simple state update | |||
| // Simple state update | |||
| setQcItems(prev => | |||
| prev.map(item => | |||
| item.id === params.id | |||
| @@ -490,10 +490,10 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | |||
| // 修改:失败项目自动显示 Lot Required Pick Qty | |||
| value={!params.row.qcPassed ? (0) : 0} | |||
| disabled={params.row.qcPassed} | |||
| // ✅ 移除 onChange,因为数量是固定的 | |||
| // 移除 onChange,因为数量是固定的 | |||
| // onChange={(e) => { | |||
| // const v = e.target.value; | |||
| // const next = v === "" ? undefined : Number(v); | |||
| @@ -535,7 +535,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| [t], | |||
| ); | |||
| // ✅ Add stable update function | |||
| // Add stable update function | |||
| const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => { | |||
| setQcItems(prevItems => | |||
| prevItems.map(item => | |||
| @@ -546,16 +546,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| ); | |||
| }, []); | |||
| // ✅ Remove duplicate functions | |||
| // Remove duplicate functions | |||
| const getRowId = useCallback((row: any) => { | |||
| return row.id; // Just use the original ID | |||
| }, []); | |||
| // ✅ Remove complex sorting logic | |||
| // Remove complex sorting logic | |||
| // const stableQcItems = useMemo(() => { ... }); // Remove | |||
| // const sortedQcItems = useMemo(() => { ... }); // Remove | |||
| // ✅ Use qcItems directly in DataGrid | |||
| // Use qcItems directly in DataGrid | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| @@ -593,9 +593,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| rows={qcItems} // ✅ Use qcItems directly | |||
| rows={qcItems} // Use qcItems directly | |||
| autoHeight | |||
| getRowId={getRowId} // ✅ Simple row ID function | |||
| getRowId={getRowId} // Simple row ID function | |||
| /> | |||
| </Grid> | |||
| </> | |||
| @@ -636,7 +636,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| /> | |||
| {/* ✅ Combirne options 2 & 3 into one */} | |||
| {/* Combirne options 2 & 3 into one */} | |||
| <FormControlLabel | |||
| value="2" | |||
| control={<Radio />} | |||
| @@ -649,7 +649,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| </FormControl> | |||
| </Grid> | |||
| {/* ✅ Show escalation component when QC Decision = 2 (Report and Re-pick) */} | |||
| {/* Show escalation component when QC Decision = 2 (Report and Re-pick) */} | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| @@ -568,7 +568,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| return; | |||
| } | |||
| // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||
| // 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||
| // if (!data.type) { | |||
| // alert(t("Please select product type")); | |||
| // return; | |||
| @@ -625,7 +625,7 @@ const handleQtyBlur = useCallback((itemId: number) => { | |||
| } | |||
| } | |||
| // ✅ 修复:自动使用 "Consumable" 作为默认 type | |||
| // 修复:自动使用 "Consumable" 作为默认 type | |||
| const pickOrderData: SavePickOrderRequest = { | |||
| type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable" | |||
| targetDate: formattedTargetDate, | |||
| @@ -72,18 +72,18 @@ const MachineScanner: React.FC<MachineScannerProps> = ({ | |||
| try { | |||
| let machineCode: string; | |||
| // ✅ 尝试解析 JSON | |||
| // 尝试解析 JSON | |||
| try { | |||
| const scannedObj: MachineQrCode = JSON.parse(scannedInput); | |||
| machineCode = scannedObj.code; | |||
| } catch (jsonError) { | |||
| // ✅ 如果不是 JSON,尝试从花括号中提取 | |||
| // 如果不是 JSON,尝试从花括号中提取 | |||
| const match = scannedInput.match(/\{([^}?]+)\??}?/); | |||
| if (match && match[1]) { | |||
| machineCode = match[1].trim(); | |||
| console.log("Extracted machine code from braces:", machineCode); | |||
| } else { | |||
| // ✅ 如果没有花括号,直接使用输入值 | |||
| // 如果没有花括号,直接使用输入值 | |||
| machineCode = scannedInput.replace(/[{}?]/g, '').trim(); | |||
| console.log("Using plain machine code:", machineCode); | |||
| } | |||
| @@ -94,7 +94,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({ | |||
| return; | |||
| } | |||
| // ✅ 首先尝试从 API 获取 | |||
| // 首先尝试从 API 获取 | |||
| const response = await isCorrectMachineUsed(machineCode); | |||
| if (response.message === "Success") { | |||
| @@ -109,7 +109,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({ | |||
| target.value = ""; | |||
| setScanError(null); | |||
| } else { | |||
| // ✅ 如果 API 失败,尝试从本地默认数据查找 | |||
| // 如果 API 失败,尝试从本地默认数据查找 | |||
| const localMachine = machineDatabase.find( | |||
| (m) => m.code.toLowerCase() === machineCode.toLowerCase() | |||
| ); | |||
| @@ -125,7 +125,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({ | |||
| target.value = ""; | |||
| setScanError(null); | |||
| console.log("✅ Used local machine data:", localMachine); | |||
| console.log(" Used local machine data:", localMachine); | |||
| } else { | |||
| setScanError( | |||
| "Machine not found. Please check the code and try again." | |||
| @@ -13,7 +13,7 @@ import { | |||
| import CloseIcon from "@mui/icons-material/Close"; | |||
| import { isOperatorExist } from "@/app/api/jo/actions"; | |||
| import { OperatorQrCode } from "./types"; | |||
| // ✅ 新增:导入 user API | |||
| // 新增:导入 user API | |||
| import { fetchUserDetails } from "@/app/api/user/actions"; | |||
| import { fetchNameList } from "@/app/api/user/actions"; | |||
| @@ -71,19 +71,19 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
| console.log("Raw input:", usernameInput); | |||
| try { | |||
| // ✅ 检查是否是测试快捷格式 {2fitest<id>} | |||
| // 检查是否是测试快捷格式 {2fitest<id>} | |||
| const testMatch = usernameInput.match(/\{2fitest(\d+)\??}?/i); | |||
| if (testMatch && testMatch[1]) { | |||
| const userId = parseInt(testMatch[1]); | |||
| console.log(`🧪 Test mode: Fetching user with ID ${userId} from API`); | |||
| try { | |||
| // ✅ 方案 1:使用 fetchNameList 获取所有用户,然后找到对应 ID | |||
| // 方案 1:使用 fetchNameList 获取所有用户,然后找到对应 ID | |||
| const nameList = await fetchNameList(); | |||
| const matchedUser = nameList.find(user => user.id === userId); | |||
| if (matchedUser) { | |||
| // ✅ 将 NameList 转换为 Operator 格式 | |||
| // 将 NameList 转换为 Operator 格式 | |||
| const operator: Operator = { | |||
| id: matchedUser.id, | |||
| name: matchedUser.name, | |||
| @@ -100,7 +100,7 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
| target.value = ""; | |||
| setScanError(null); | |||
| console.log(`✅ Added operator from API:`, operator); | |||
| console.log(` Added operator from API:`, operator); | |||
| return; | |||
| } else { | |||
| setScanError( | |||
| @@ -121,18 +121,18 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({ | |||
| let username: string; | |||
| // ✅ 尝试解析 JSON | |||
| // 尝试解析 JSON | |||
| try { | |||
| const usernameObj: OperatorQrCode = JSON.parse(usernameInput); | |||
| username = usernameObj.username; | |||
| } catch (jsonError) { | |||
| // ✅ 如果不是 JSON,尝试从花括号中提取 | |||
| // 如果不是 JSON,尝试从花括号中提取 | |||
| const match = usernameInput.match(/\{([^}?]+)\??}?/); | |||
| if (match && match[1]) { | |||
| username = match[1].trim(); | |||
| console.log("Extracted username from braces:", username); | |||
| } else { | |||
| // ✅ 如果没有花括号,直接使用输入值 | |||
| // 如果没有花括号,直接使用输入值 | |||
| username = usernameInput.replace(/[{}?]/g, '').trim(); | |||
| console.log("Using plain username:", username); | |||
| } | |||
| @@ -0,0 +1,656 @@ | |||
| "use client"; | |||
| import React, { useCallback, useEffect, useState, useRef } from "react"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Paper, | |||
| Stack, | |||
| Typography, | |||
| TextField, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Chip, | |||
| Card, | |||
| CardContent, | |||
| CircularProgress, | |||
| } from "@mui/material"; | |||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Operator, Machine } from "@/app/api/jo"; | |||
| import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | |||
| import CheckCircleIcon from "@mui/icons-material/CheckCircle"; | |||
| import dayjs from "dayjs"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { | |||
| fetchProductProcessById, | |||
| fetchProductProcessLines, | |||
| updateProductProcessLineQrscan, | |||
| fetchProductProcessLineDetail, | |||
| ProductProcessResponse, | |||
| ProductProcessLineResponse, | |||
| startProductProcessLine | |||
| } from "@/app/api/jo/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| // 添加设备数据库(从 MachineScanner.tsx) | |||
| const machineDatabase: Machine[] = [ | |||
| { id: 1, name: "CNC Mill #1", code: "CNC001", qrCode: "QR-CNC001" }, | |||
| { id: 2, name: "Lathe #2", code: "LAT002", qrCode: "QR-LAT002" }, | |||
| { id: 3, name: "Press #3", code: "PRS003", qrCode: "QR-PRS003" }, | |||
| { id: 4, name: "Welder #4", code: "WLD004", qrCode: "QR-WLD004" }, | |||
| { id: 5, name: "Drill Press #5", code: "DRL005", qrCode: "QR-DRL005" }, | |||
| ]; | |||
| interface ProcessLine { | |||
| id: number; | |||
| seqNo: number; | |||
| name: string; | |||
| description?: string; | |||
| equipmentType?: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| outputFromProcessQty?: number; | |||
| outputFromProcessUom?: string; | |||
| defectQty?: number; | |||
| scrapQty?: number; | |||
| byproductName?: string; | |||
| byproductQty?: number; | |||
| handlerId?: number; // 添加 handlerId | |||
| } | |||
| interface ProductProcessDetailProps { | |||
| processId: number; | |||
| onBack: () => void; | |||
| } | |||
| const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({ | |||
| processId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| // 基本信息 | |||
| const [processData, setProcessData] = useState<any>(null); | |||
| const [lines, setLines] = useState<ProcessLine[]>([]); | |||
| const [loading, setLoading] = useState(false); | |||
| // 选中的 line 和执行状态 | |||
| const [selectedLineId, setSelectedLineId] = useState<number | null>(null); | |||
| const [isExecutingLine, setIsExecutingLine] = useState(false); | |||
| // 扫描器状态 | |||
| const [isManualScanning, setIsManualScanning] = useState(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [scannedOperators, setScannedOperators] = useState<Operator[]>([]); | |||
| const [scannedMachines, setScannedMachines] = useState<Machine[]>([]); | |||
| // 产出表单 | |||
| const [outputData, setOutputData] = useState({ | |||
| byproductName: "", | |||
| byproductQty: "", | |||
| byproductUom: "", | |||
| scrapQty: "", | |||
| scrapUom: "", | |||
| defectQty: "", | |||
| defectUom: "", | |||
| outputFromProcessQty: "", | |||
| outputFromProcessUom: "", | |||
| }); | |||
| // 处理 QR 码扫描 | |||
| // 处理 QR 码扫描 | |||
| const processQrCode = useCallback((qrValue: string) => { | |||
| // 操作员格式:{2fitestu1} - 键盘模拟输入(测试用) | |||
| if (qrValue.match(/\{2fitestu(\d+)\}/)) { | |||
| const match = qrValue.match(/\{2fitestu(\d+)\}/); | |||
| const userId = parseInt(match![1]); | |||
| // 调用 API 获取用户信息 | |||
| fetchNameList().then((users: NameList[]) => { | |||
| const user = users.find((u: NameList) => u.id === userId); | |||
| if (user) { | |||
| setScannedOperators([{ | |||
| id: user.id, | |||
| name: user.name, | |||
| username: user.name | |||
| }]); | |||
| updateProductProcessLineQrscan({ | |||
| lineId: selectedLineId || 0 as number, | |||
| operatorId: user.id, | |||
| }); | |||
| } | |||
| }); | |||
| return; | |||
| } | |||
| // 设备格式:{2fiteste1} - 键盘模拟输入(测试用) | |||
| if (qrValue.match(/\{2fiteste(\d+)\}/)) { | |||
| const match = qrValue.match(/\{2fiteste(\d+)\}/); | |||
| const equipmentId = parseInt(match![1]); | |||
| // 使用本地设备数据库 | |||
| const machine = machineDatabase.find((m: Machine) => m.id === equipmentId); | |||
| if (machine) { | |||
| setScannedMachines([machine]); | |||
| } | |||
| updateProductProcessLineQrscan({ | |||
| lineId: selectedLineId || 0 as number, | |||
| equipmentId: equipmentId, | |||
| }).then((res) => { | |||
| console.log(res); | |||
| }); | |||
| return; | |||
| } | |||
| // 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1" | |||
| const trimmedValue = qrValue.trim(); | |||
| // 检查 operatorId 格式 | |||
| const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i); | |||
| if (operatorMatch) { | |||
| const operatorId = parseInt(operatorMatch[1]); | |||
| fetchNameList().then((users: NameList[]) => { | |||
| const user = users.find((u: NameList) => u.id === operatorId); | |||
| if (user) { | |||
| setScannedOperators([{ | |||
| id: user.id, | |||
| name: user.name, | |||
| username: user.name | |||
| }]); | |||
| updateProductProcessLineQrscan({ | |||
| lineId: selectedLineId || 0 as number, | |||
| operatorId: user.id, | |||
| }); | |||
| } else { | |||
| console.warn(`User with ID ${operatorId} not found`); | |||
| } | |||
| }); | |||
| return; | |||
| } | |||
| // 检查 equipmentId 格式 | |||
| const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i); | |||
| if (equipmentMatch) { | |||
| const equipmentId = parseInt(equipmentMatch[1]); | |||
| const machine = machineDatabase.find((m: Machine) => m.id === equipmentId); | |||
| if (machine) { | |||
| setScannedMachines([machine]); | |||
| } | |||
| updateProductProcessLineQrscan({ | |||
| lineId: selectedLineId || 0 as number, | |||
| equipmentId: equipmentId, | |||
| }).then((res) => { | |||
| console.log(res); | |||
| }); | |||
| return; | |||
| } | |||
| // 其他格式处理(JSON、普通文本等) | |||
| try { | |||
| const qrData = JSON.parse(qrValue); | |||
| // TODO: 处理 JSON 格式的 QR 码 | |||
| } catch { | |||
| // 普通文本格式 | |||
| // TODO: 处理普通文本格式 | |||
| } | |||
| }, [selectedLineId]); | |||
| // 处理 QR 码扫描效果 | |||
| useEffect(() => { | |||
| if (isManualScanning && qrValues.length > 0 && isExecutingLine) { | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| if (processedQrCodes.has(latestQr)) { | |||
| return; | |||
| } | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| processQrCode(latestQr); | |||
| } | |||
| }, [qrValues, isManualScanning, isExecutingLine, processedQrCodes, processQrCode]); | |||
| // 开始扫描 | |||
| const handleStartScan = useCallback(() => { | |||
| setIsManualScanning(true); | |||
| setProcessedQrCodes(new Set()); | |||
| startScan(); | |||
| }, [startScan]); | |||
| // 停止扫描 | |||
| const handleStopScan = useCallback(() => { | |||
| setIsManualScanning(false); | |||
| stopScan(); | |||
| resetScan(); | |||
| }, [stopScan, resetScan]); | |||
| // 获取 process 和 lines 数据 | |||
| const fetchProcessDetail = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| console.log(`🔍 Loading process detail for ID: ${processId}`); | |||
| const data = await fetchProductProcessLineDetail(processId); | |||
| setProcessData(data); | |||
| const linesData = await fetchProductProcessLineDetail(processId); | |||
| setLines([linesData]); | |||
| console.log(" Process data loaded:", data); | |||
| console.log(" Lines loaded:", linesData); | |||
| } catch (error) { | |||
| console.error("❌ Error loading process detail:", error); | |||
| alert(`无法加载生产流程 ID ${processId}。该记录可能不存在。`); | |||
| onBack(); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [processId, onBack]); | |||
| useEffect(() => { | |||
| fetchProcessDetail(); | |||
| }, [fetchProcessDetail]); | |||
| // 开始执行某个 line | |||
| const handleStartLine = async (lineId: number) => { | |||
| if (!currentUserId) { | |||
| alert("Please login first!"); | |||
| return; | |||
| } | |||
| try { | |||
| // 使用 Server Action 而不是直接 fetch | |||
| await startProductProcessLine(lineId, currentUserId); | |||
| console.log(` Starting line ${lineId} with handlerId: ${currentUserId}`); | |||
| setSelectedLineId(lineId); | |||
| setIsExecutingLine(true); | |||
| setScannedOperators([]); | |||
| setScannedMachines([]); | |||
| setOutputData({ | |||
| byproductName: "", | |||
| byproductQty: "", | |||
| byproductUom: "", | |||
| scrapQty: "", | |||
| scrapUom: "", | |||
| defectQty: "", | |||
| defectUom: "", | |||
| outputFromProcessQty: "", | |||
| outputFromProcessUom: "", | |||
| }); | |||
| // 刷新数据 | |||
| await fetchProcessDetail(); | |||
| } catch (error) { | |||
| console.error("Error starting line:", error); | |||
| alert("Failed to start line. Please try again."); | |||
| } | |||
| }; | |||
| // 提交产出数据 | |||
| const handleSubmitOutput = async () => { | |||
| if (!selectedLineId) return; | |||
| if (scannedOperators.length === 0 || scannedMachines.length === 0) { | |||
| alert("Please scan operator and machine first!"); | |||
| return; | |||
| } | |||
| try { | |||
| const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8090'}/product-process/lines/${selectedLineId}/output`, { | |||
| method: 'PUT', | |||
| headers: { 'Content-Type': 'application/json' }, | |||
| body: JSON.stringify({ | |||
| outputQty: parseFloat(outputData.outputFromProcessQty) || 0, | |||
| outputUom: outputData.outputFromProcessUom, | |||
| defectQty: parseFloat(outputData.defectQty) || 0, | |||
| defectUom: outputData.defectUom, | |||
| scrapQty: parseFloat(outputData.scrapQty) || 0, | |||
| scrapUom: outputData.scrapUom, | |||
| byproductName: outputData.byproductName, | |||
| byproductQty: parseFloat(outputData.byproductQty) || 0, | |||
| byproductUom: outputData.byproductUom, | |||
| }), | |||
| }); | |||
| if (response.ok) { | |||
| console.log(" Output data submitted successfully"); | |||
| setIsExecutingLine(false); | |||
| setSelectedLineId(null); | |||
| handleStopScan(); // 停止扫描 | |||
| fetchProcessDetail(); // 刷新数据 | |||
| } | |||
| } catch (error) { | |||
| console.error("Error submitting output:", error); | |||
| } | |||
| }; | |||
| const selectedLine = lines.find(l => l.id === selectedLineId); | |||
| if (loading) { | |||
| return ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||
| <CircularProgress/> | |||
| </Box> | |||
| ); | |||
| } | |||
| return ( | |||
| <Box> | |||
| {/* 返回按钮 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| <Button variant="outlined" onClick={onBack}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| {/* ========== 第一部分:基本信息 ========== */} | |||
| <Paper sx={{ p: 3, mb: 3 }}> | |||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | |||
| {t("Production Process Information")} | |||
| </Typography> | |||
| <Stack spacing={2} direction="row" useFlexGap flexWrap="wrap"> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Process Code")}:</strong> {processData?.productProcessCode} | |||
| </Typography> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Status")}:</strong>{" "} | |||
| <Chip | |||
| label={t(processData?.status || 'pending')} | |||
| color={processData?.status === 'completed' ? 'success' : 'primary'} | |||
| size="small" | |||
| /> | |||
| </Typography> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)} | |||
| </Typography> | |||
| <Typography variant="subtitle1"> | |||
| <strong>{t("Total Steps")}:</strong> {lines.length} | |||
| </Typography> | |||
| </Stack> | |||
| </Paper> | |||
| {/* ========== 第二部分:Process Lines ========== */} | |||
| <Paper sx={{ p: 3 }}> | |||
| <Typography variant="h6" gutterBottom fontWeight="bold"> | |||
| {t("Production Process Steps")} | |||
| </Typography> | |||
| {!isExecutingLine ? ( | |||
| /* ========== 步骤列表视图 ========== */ | |||
| <TableContainer> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Seq")}</TableCell> | |||
| <TableCell>{t("Step Name")}</TableCell> | |||
| <TableCell>{t("Description")}</TableCell> | |||
| <TableCell>{t("Equipment Type")}</TableCell> | |||
| <TableCell align="center">{t("Status")}</TableCell> | |||
| <TableCell align="center">{t("Action")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {lines.map((line) => ( | |||
| <TableRow key={line.id}> | |||
| <TableCell>{line.seqNo}</TableCell> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{line.name}</Typography> | |||
| </TableCell> | |||
| <TableCell>{line.description}</TableCell> | |||
| <TableCell>{line.equipmentType}</TableCell> | |||
| <TableCell align="center"> | |||
| {line.endTime ? ( | |||
| <Chip label={t("Completed")} color="success" size="small" /> | |||
| ) : line.startTime ? ( | |||
| <Chip label={t("In Progress")} color="primary" size="small" /> | |||
| ) : ( | |||
| <Chip label={t("Pending")} color="default" size="small" /> | |||
| )} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={() => handleStartLine(line.id)} | |||
| disabled={!!line.endTime} | |||
| > | |||
| {line.endTime ? t("Completed") : t("Start")} | |||
| </Button> | |||
| </TableCell> | |||
| </TableRow> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| ) : ( | |||
| /* ========== 步骤执行视图 ========== */ | |||
| <Box> | |||
| {/* 当前步骤信息 */} | |||
| <Card sx={{ mb: 3, bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main' }}> | |||
| <CardContent> | |||
| <Typography variant="h6" color="primary.main" gutterBottom> | |||
| {t("Executing")}: {selectedLine?.name} (Seq: {selectedLine?.seqNo}) | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {selectedLine?.description} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Equipment")}: {selectedLine?.equipmentType} | |||
| </Typography> | |||
| </CardContent> | |||
| </Card> | |||
| <Stack spacing={3}> | |||
| {/* 合并的扫描器 */} | |||
| <Paper sx={{ p: 3, mb: 3 }}> | |||
| <Typography variant="h6" gutterBottom> | |||
| {t("Scan Operator & Equipment")} | |||
| </Typography> | |||
| <Stack spacing={2}> | |||
| {/* 操作员扫描 */} | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {scannedOperators.length > 0 | |||
| ? `${t("Operator")}: ${scannedOperators[0].name || scannedOperators[0].username}` | |||
| : t("Please scan operator code") | |||
| } | |||
| </Typography> | |||
| </Box> | |||
| {/* 设备扫描 */} | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {scannedMachines.length > 0 | |||
| ? `${t("Equipment")}: ${scannedMachines[0].name || scannedMachines[0].code}` | |||
| : t("Please scan equipment code") | |||
| } | |||
| </Typography> | |||
| </Box> | |||
| {/* 单个扫描按钮 */} | |||
| <Button | |||
| variant={isManualScanning ? "outlined" : "contained"} | |||
| startIcon={<QrCodeIcon />} | |||
| onClick={isManualScanning ? handleStopScan : handleStartScan} | |||
| color={isManualScanning ? "secondary" : "primary"} | |||
| > | |||
| {isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")} | |||
| </Button> | |||
| </Stack> | |||
| </Paper> | |||
| {/* ========== 产出输入表单 ========== */} | |||
| {scannedOperators.length > 0 && scannedMachines.length > 0 && ( | |||
| <Paper sx={{ p: 3, bgcolor: 'grey.50' }}> | |||
| <Typography variant="h6" gutterBottom fontWeight={600}> | |||
| {t("Production Output Data Entry")} | |||
| </Typography> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell width="30%">{t("Type")}</TableCell> | |||
| <TableCell width="35%">{t("Quantity")}</TableCell> | |||
| <TableCell width="35%">{t("Unit of Measure")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {/* 步骤收成 */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Typography fontWeight={500}>{t("Output from Process")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessQty} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, outputFromProcessQty: e.target.value }))} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.outputFromProcessUom} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, outputFromProcessUom: e.target.value }))} | |||
| placeholder="KG, L, PCS..." | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* 副产品 */} | |||
| <TableRow> | |||
| <TableCell> | |||
| <Stack> | |||
| <Typography fontWeight={500}>{t("By-product")}</Typography> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductName} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, byproductName: e.target.value }))} | |||
| placeholder={t("By-product name")} | |||
| sx={{ mt: 1 }} | |||
| /> | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductQty} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, byproductQty: e.target.value }))} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.byproductUom} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, byproductUom: e.target.value }))} | |||
| placeholder="KG, L, PCS..." | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* 次品 */} | |||
| <TableRow sx={{ bgcolor: 'warning.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectQty} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, defectQty: e.target.value }))} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.defectUom} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, defectUom: e.target.value }))} | |||
| placeholder="KG, L, PCS..." | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| {/* 废品 */} | |||
| <TableRow sx={{ bgcolor: 'error.50' }}> | |||
| <TableCell> | |||
| <Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapQty} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, scrapQty: e.target.value }))} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| fullWidth | |||
| size="small" | |||
| value={outputData.scrapUom} | |||
| onChange={(e) => setOutputData(prev => ({ ...prev, scrapUom: e.target.value }))} | |||
| placeholder="KG, L, PCS..." | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| {/* 提交按钮 */} | |||
| <Box sx={{ mt: 3, display: 'flex', gap: 2 }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| setIsExecutingLine(false); | |||
| setSelectedLineId(null); | |||
| handleStopScan(); | |||
| }} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<CheckCircleIcon />} | |||
| onClick={handleSubmitOutput} | |||
| > | |||
| {t("Complete Step")} | |||
| </Button> | |||
| </Box> | |||
| </Paper> | |||
| )} | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| </Paper> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ProductionProcessDetail; | |||
| @@ -0,0 +1,185 @@ | |||
| "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 { useTranslation } from "react-i18next"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import dayjs from "dayjs"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { | |||
| fetchAllJoborderProductProcessInfo, | |||
| AllJoborderProductProcessInfoResponse, | |||
| } from "@/app/api/jo/actions"; | |||
| interface ProductProcessListProps { | |||
| onSelectProcess: (processId: number) => void; | |||
| } | |||
| const PER_PAGE = 6; | |||
| const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess }) => { | |||
| const { t } = useTranslation(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const [loading, setLoading] = useState(false); | |||
| const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| const fetchProcesses = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const data = await fetchAllJoborderProductProcessInfo(); | |||
| setProcesses(data || []); | |||
| setPage(0); | |||
| } catch (e) { | |||
| console.error(e); | |||
| setProcesses([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| fetchProcesses(); | |||
| }, [fetchProcesses]); | |||
| const startIdx = page * PER_PAGE; | |||
| const paged = processes.slice(startIdx, startIdx + PER_PAGE); | |||
| return ( | |||
| <Box> | |||
| {loading ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total processes")}: {processes.length} | |||
| </Typography> | |||
| <Grid container spacing={2}> | |||
| {paged.map((process) => { | |||
| const status = String(process.status || ""); | |||
| const statusLower = status.toLowerCase(); | |||
| const statusColor = | |||
| statusLower === "completed" | |||
| ? "success" | |||
| : statusLower === "in_progress" || statusLower === "processing" | |||
| ? "primary" | |||
| : "default"; | |||
| const finishedCount = | |||
| (process as any).finishedProductProcessLineCount ?? | |||
| (process as any).FinishedProductProcessLineCount ?? | |||
| 0; | |||
| const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0; | |||
| const linesWithStatus = (process.lines || []).filter( | |||
| (l) => String(l.status ?? "").trim() !== "" | |||
| ); | |||
| const dateDisplay = process.date | |||
| ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) | |||
| : "-"; | |||
| const jobOrderCode = | |||
| (process as any).jobOrderCode ?? | |||
| (process.jobOrderId ? `JO-${process.jobOrderId}` : "N/A"); | |||
| const inProgressLines = (process.lines || []) | |||
| .filter(l => String(l.status ?? "").trim() !== "") | |||
| .filter(l => String(l.status).toLowerCase() === "in_progress"); | |||
| return ( | |||
| <Grid key={process.id} item xs={12} sm={6} md={4}> | |||
| <Card | |||
| sx={{ | |||
| minHeight: 160, | |||
| maxHeight: 240, | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| }} | |||
| > | |||
| <CardContent | |||
| sx={{ | |||
| pb: 1, | |||
| flexGrow: 1, // let content take remaining height | |||
| overflow: "auto", // allow scroll when content exceeds | |||
| }} | |||
| > | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center"> | |||
| <Box sx={{ minWidth: 0 }}> | |||
| <Typography variant="subtitle1"> | |||
| {process.productProcessCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Job Order")}: {jobOrderCode} | |||
| </Typography> | |||
| </Box> | |||
| <Chip size="small" label={t(status)} color={statusColor as any} /> | |||
| </Stack> | |||
| {statusLower !== "pending" && linesWithStatus.length > 0 && ( | |||
| <Box sx={{ mt: 1 }}> | |||
| <Typography variant="body2" fontWeight={600}> | |||
| {t("Finished lines")}: {finishedCount} / {totalCount} | |||
| </Typography> | |||
| {inProgressLines.length > 0 && ( | |||
| <Box sx={{ mt: 1 }}> | |||
| {inProgressLines.map(line => ( | |||
| <Typography key={line.id} variant="caption" color="text.secondary" display="block"> | |||
| {t("Operator")}: {line.operatorName || "-"} <br /> | |||
| {t("Equipment")}: {line.equipmentName || "-"} | |||
| </Typography> | |||
| ))} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| )} | |||
| </CardContent> | |||
| <CardActions sx={{ pt: 0.5 }}> | |||
| <Button variant="contained" size="small" onClick={() => onSelectProcess(process.id)}> | |||
| {t("View Details")} | |||
| </Button> | |||
| <Box sx={{ flex: 1 }} /> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Lines")}: {totalCount} | |||
| </Typography> | |||
| </CardActions> | |||
| </Card> | |||
| </Grid> | |||
| ); | |||
| })} | |||
| </Grid> | |||
| {processes.length > 0 && ( | |||
| <TablePagination | |||
| component="div" | |||
| count={processes.length} | |||
| page={page} | |||
| rowsPerPage={PER_PAGE} | |||
| onPageChange={(e, p) => setPage(p)} | |||
| rowsPerPageOptions={[PER_PAGE]} | |||
| /> | |||
| )} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ProductProcessList; | |||
| @@ -0,0 +1,67 @@ | |||
| "use client"; | |||
| import React, { useState, useEffect, useCallback } from "react"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; | |||
| import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | |||
| import { | |||
| fetchProductProcesses, | |||
| fetchProductProcessLines, | |||
| ProductProcessLineResponse | |||
| } from "@/app/api/jo/actions"; | |||
| const ProductionProcessPage = () => { | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const checkAndRedirectToDetail = useCallback(async () => { | |||
| if (!currentUserId) return; | |||
| try { | |||
| // 获取所有 processes | |||
| const processes = await fetchProductProcesses(); | |||
| // 获取所有 lines 并检查是否有匹配的 | |||
| for (const process of processes.content || []) { | |||
| const lines = await fetchProductProcessLines(process.id); | |||
| const pendingLine = lines.find((line: ProductProcessLineResponse) => | |||
| line.handlerId === currentUserId && | |||
| !line.endTime && | |||
| line.startTime | |||
| ); | |||
| if (pendingLine) { | |||
| setSelectedProcessId(process.id); | |||
| break; | |||
| } | |||
| } | |||
| } catch (error) { | |||
| console.error("Error checking pending lines:", error); | |||
| } | |||
| }, [currentUserId]); | |||
| useEffect(() => { | |||
| if (currentUserId && !selectedProcessId) { | |||
| // 检查是否有当前用户的 pending line | |||
| checkAndRedirectToDetail(); | |||
| } | |||
| }, [currentUserId, selectedProcessId, checkAndRedirectToDetail]); | |||
| if (selectedProcessId !== null) { | |||
| return ( | |||
| <ProductionProcessDetail | |||
| processId={selectedProcessId} | |||
| onBack={() => setSelectedProcessId(null)} | |||
| /> | |||
| ); | |||
| } | |||
| return ( | |||
| <ProductionProcessList | |||
| onSelectProcess={(id) => setSelectedProcessId(id)} | |||
| /> | |||
| ); | |||
| }; | |||
| export default ProductionProcessPage; | |||