From 358a15b25ce8b6007e485a4d1ff3f6c824e70577 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 12 Dec 2025 15:23:02 +0800 Subject: [PATCH] update pick0, search jo --- src/app/api/jo/actions.ts | 17 +- src/app/api/po/actions.ts | 3 + src/components/DoDetail/DoDetail.tsx | 23 +- src/components/DoSearch/DoSearch.tsx | 42 +++- .../GoodPickExecutiondetail.tsx | 123 +++++----- src/components/JoSearch/JoCreateFormModal.tsx | 4 + src/components/JoSearch/JoSearch.tsx | 213 ++++++++---------- src/components/Jodetail/JoPickOrderList.tsx | 10 +- src/components/Jodetail/JobPickExecution.tsx | 83 ++++++- .../Jodetail/JobPickExecutionForm.tsx | 66 ++++-- src/components/Jodetail/JodetailSearch.tsx | 19 +- .../Jodetail/newJobPickExecution.tsx | 93 +++++++- .../ProductionProcessJobOrderDetail.tsx | 96 ++++++-- src/i18n/zh/common.json | 1 + src/i18n/zh/jo.json | 1 + 15 files changed, 527 insertions(+), 267 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 3050215..67e1a29 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -683,7 +683,14 @@ export const fetchProductProcessById = cache(async (id: number) => { } ); }); - +export const updateProductProcessPriority = cache(async (productProcessId: number, productionPriority: number) => { + return serverFetchJson( + `${BASE_API_URL}/product-process/Demo/Process/update/priority/${productProcessId}/${productionPriority}`, + { + method: "POST", + } + ); +}); // 根据 Job Order ID 查询 export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number) => { return serverFetchJson( @@ -879,7 +886,10 @@ export const isCorrectMachineUsed = async (machineCode: string) => { export const fetchJos = cache(async (data?: SearchJoResultRequest) => { const queryStr = convertObjToURLSearchParams(data) console.log("queryStr", queryStr) - const response = serverFetchJson( + const fullUrl = `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`; + console.log("fetchJos full URL:", fullUrl); + console.log("fetchJos BASE_API_URL:", BASE_API_URL); + const response = await serverFetchJson( `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, { method: "GET", @@ -889,7 +899,8 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => { } } ) - + console.log("fetchJos response:", response) + return response }) diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts index cdbfbc4..4bae861 100644 --- a/src/app/api/po/actions.ts +++ b/src/app/api/po/actions.ts @@ -204,6 +204,9 @@ export const fetchPoListClient = cache( async (queryParams?: Record) => { if (queryParams) { const queryString = new URLSearchParams(queryParams).toString(); + const fullUrl = `${BASE_API_URL}/po/list?${queryString}`; + console.log("fetchPoListClient full URL:", fullUrl); + console.log("fetchPoListClient BASE_API_URL:", BASE_API_URL); return serverFetchJson>( `${BASE_API_URL}/po/list?${queryString}`, { diff --git a/src/components/DoDetail/DoDetail.tsx b/src/components/DoDetail/DoDetail.tsx index a8d3a14..da1d85b 100644 --- a/src/components/DoDetail/DoDetail.tsx +++ b/src/components/DoDetail/DoDetail.tsx @@ -9,7 +9,7 @@ import { useCallback, useState } from "react"; import { Button, Stack, Typography, Box, Alert } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import StartIcon from "@mui/icons-material/Start"; -import { releaseDo, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions"; +import { releaseDo,startBatchReleaseAsyncSingle, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions"; import DoInfoCard from "./DoInfoCard"; import DoLineTable from "./DoLineTable"; import { useSession } from "next-auth/react"; @@ -41,7 +41,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); const handleBack = useCallback(() => { router.replace(`/do`) - }, []) + }, [router]) const handleRelease = useCallback(async () => { try { @@ -57,12 +57,16 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); // setServerError("User session not found. Please login again."); // return; //} - + /* const response = await releaseDo({ id: id, //userId: currentUserId // Pass user ID from session }) - + */ + const response = await startBatchReleaseAsyncSingle({ + doId: id, + userId: currentUserId ?? 0 + }) if (response) { formProps.setValue("status", response.entity.status) setSuccessMessage(t("DO released successfully! Pick orders created.")) @@ -168,8 +172,8 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); )} - {/*{ - formProps.watch("status")?.toLowerCase() === "pending" && ( + + {formProps.watch("status")?.toLowerCase() === "pending" && ( ) } }, - ], [inventoryData, detailedJos] + ], [t, inventoryData, detailedJos] ) + // 按照 PoSearch 的模式:创建 newPageFetch 函数 + const newPageFetch = useCallback( + async ( + pagingController: { pageNum: number; pageSize: number }, + filterArgs: SearchJoResultRequest, + ) => { + const params: SearchJoResultRequest = { + ...filterArgs, + pageNum: pagingController.pageNum - 1, + pageSize: pagingController.pageSize, + }; + const response = await fetchJos(params); + console.log("newPageFetch params:", params) + console.log("newPageFetch response:", response) + if (response && response.records) { + console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); + setTotalCount(response.total); + // 后端已经按 id DESC 排序,不需要再次排序 + setFilteredJos(response.records); + console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); + } else { + console.warn("newPageFetch - no response or no records"); + setFilteredJos([]); + } + }, + [], + ); + + // 按照 PoSearch 的模式:使用相同的 useEffect 逻辑 + useEffect(() => { + newPageFetch(pagingController, inputs); + }, [newPageFetch, pagingController, inputs]); + const handleUpdate = useCallback(async (jo: JobOrder) => { console.log(jo); try { - // setIsUploading(true) if (jo.id) { const response = await updateJo({ id: jo.id, status: "storing" }); console.log(`%c Updated JO:`, "color:lime", response); @@ -252,64 +274,22 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT productLotNo: jo?.code, productionDate: arrayToDateString(dayjs(), "input"), jobOrderId: jo?.id, - // acceptedQty: secondReceiveQty || 0, - // acceptedQty: row.acceptedQty, }; const res = await createStockInLine(postData); console.log(`%c Created Stock In Line`, "color:lime", res); msg(t("update success")); - refetchData(defaultInputs, "search"); + // 重置为默认输入,让 useEffect 自动触发 + setInputs(defaultInputs); + setPagingController(defaultPagingController); } - } catch (e) { - // backend error - // setServerError(t("An error has occurred. Please try again later.")); console.log(e); } finally { // setIsUploading(false) } - }, []) + }, [defaultInputs, t]) - const refetchData = useCallback(async ( - query: Record | SearchJoResultRequest, - actionType: "reset" | "search" | "paging", - ) => { - const params: SearchJoResultRequest = { - code: query.code, - itemName: query.itemName, - planStart: query.planStart, - planStartTo: query.planStartTo, - pageNum: pagingController.pageNum - 1, - pageSize: pagingController.pageSize, - jobTypeName: query.jobTypeName||"", - } - const response = await fetchJos(params) - - if (response) { - setTotalCount(response.total); - switch (actionType) { - case "reset": - case "search": - setFilteredJos(() => orderBy(response.records, ["id"], ["desc"])); - break; - case "paging": - setFilteredJos((fs) => - orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]), - ); - break; - } - } - }, [pagingController, setPagingController]) - - const searchDataByPage = useCallback(() => { - refetchData(inputs, "paging"); - }, [inputs,refetchData]) - /* - useEffect(() => { - searchDataByPage(); - }, [pagingController,searchDataByPage ]); - */ - const getButtonSx = (jo : JobOrder) => { // TODO put it in ActionButtons.ts + const getButtonSx = (jo : JobOrder) => { const joStatus = jo.status?.toLowerCase(); const silStatus = jo.stockInLineStatus?.toLowerCase(); let btnSx = {label:"", color:""}; @@ -317,8 +297,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break; case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break; case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break; - // case "packaging": - // case "storing": btnSx = {label: t("view putaway"), color:"secondary.main"}; break; case "storing": switch (silStatus) { case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break; @@ -342,60 +320,44 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const [openModal, setOpenModal] = useState(false); const [modalInfo, setModalInfo] = useState(); -/* - const onDetailClick = useCallback((record: JobOrder) => { - if (record.status == "processing") { - handleUpdate(record) - } else if (record.status == "storing" || record.status == "completed") { - if (record.stockInLineId != null) { - const data = { - id: record.stockInLineId, - expiryDate: arrayToDateString(dayjs().add(1, "month"), "input"), - } - setModalInfo(data); - setOpenModal(true); - } else { alert('Invalid Stock In Line Id'); } - } else { - router.push(`/jo/edit?id=${record.id}`) - } - }, []) -*/ const onDetailClick = useCallback((record: JobOrder) => { router.push(`/jo/edit?id=${record.id}`) - }, []) - const closeNewModal = useCallback(() => { - // const response = updateJo({ id: 1, status: "storing" }); - setOpenModal(false); // Close the modal first - // setTimeout(() => { - // }, 300); // Add a delay to avoid immediate re-trigger of useEffect - refetchData(defaultInputs, "search"); - }, []); + }, [router]) + const closeNewModal = useCallback(() => { + setOpenModal(false); + + setInputs(defaultInputs); + setPagingController(defaultPagingController); + }, [defaultInputs]); const onSearch = useCallback((query: Record) => { const transformedQuery = { ...query, - planStart: query.planStart ? `${query.planStart}T00:00:00` : query.planStart, + planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart, planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo, jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" }; - setInputs(() => ({ + + setInputs({ code: transformedQuery.code, itemName: transformedQuery.itemName, planStart: transformedQuery.planStart, planStartTo: transformedQuery.planStartTo, jobTypeName: transformedQuery.jobTypeName - })) - refetchData(transformedQuery, "search"); - }, []) + }); + + setPagingController(defaultPagingController); + }, [defaultInputs]) const onReset = useCallback(() => { - refetchData(defaultInputs, "paging"); - }, []) + + setInputs(defaultInputs); + setPagingController(defaultPagingController); + }, [defaultInputs]) - // Manual Create Jo Related const onOpenCreateJoModal = useCallback(() => { setIsCreateJoModalOpen(() => true) @@ -425,19 +387,21 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT onSearch={onSearch} onReset={onReset} /> - + items={filteredJos} columns={columns} setPagingController={setPagingController} pagingController={pagingController} totalCount={totalCount} - // isAutoPaging={false} + isAutoPaging={false} /> { + + }} /> = ({ defaultInputs, bomCombo, printerCombo, jobT onClose={closeNewModal} inputDetail={modalInfo} printerCombo={printerCombo} - // skipQc={true} /> } diff --git a/src/components/Jodetail/JoPickOrderList.tsx b/src/components/Jodetail/JoPickOrderList.tsx index ac6968a..a14176f 100644 --- a/src/components/Jodetail/JoPickOrderList.tsx +++ b/src/components/Jodetail/JoPickOrderList.tsx @@ -17,17 +17,19 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { useTranslation } from "react-i18next"; import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions"; import JobPickExecution from "./newJobPickExecution"; - +interface Props { + onSwitchToRecordTab?: () => void; +} const PER_PAGE = 6; -const JoPickOrderList: React.FC = () => { +const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ const { t } = useTranslation(["common", "jo"]); const [loading, setLoading] = useState(false); const [pickOrders, setPickOrders] = useState([]); const [page, setPage] = useState(0); const [selectedPickOrderId, setSelectedPickOrderId] = useState(undefined); const [selectedJobOrderId, setSelectedJobOrderId] = useState(undefined); - + const fetchPickOrders = useCallback(async () => { setLoading(true); try { @@ -62,7 +64,7 @@ const JoPickOrderList: React.FC = () => { {t("Back to List")} - + ); } diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index c86d203..ebec2ef 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -1236,17 +1236,73 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]); const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { + console.log('=== handleSubmitPickQtyWithQty called ==='); + console.log('Lot:', lot); + console.log('submitQty:', submitQty); + console.log('stockOutLineId:', lot.stockOutLineId); + if (!lot.stockOutLineId) { - console.error("No stock out line found for this lot"); + console.error("No stock out line found for this lot:", lot); + alert(`Error: No stock out line ID found for lot ${lot.lotNo}. Cannot update status.`); return; } try { - // FIXED: Calculate cumulative quantity correctly + // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 + if (submitQty === 0) { + console.log(`=== SUBMITTING ALL ZEROS CASE ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); + console.log(`Setting status to 'completed' with qty: 0`); + + const updateResult = await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'completed', + qty: 0 + }); + + console.log('Update result:', updateResult); + + if (!updateResult || (updateResult as any).code !== 'SUCCESS') { + console.error('Failed to update stock out line status:', updateResult); + throw new Error('Failed to update stock out line status'); + } + + // Check if pick order is completed + if (lot.pickOrderConsoCode) { + console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`); + + try { + const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); + console.log(` Pick order completion check result:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion: ${completionResponse.message}`); + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + } + + await fetchJobOrderData(); + console.log("All zeros submission completed successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + return; + } + + // Normal case: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; - // FIXED: Determine status based on cumulative quantity vs required quantity + // Determine status based on cumulative quantity vs required quantity let newStatus = 'partially_completed'; if (cumulativeQty >= lot.requiredQty) { @@ -1269,7 +1325,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, - qty: cumulativeQty // Use cumulative quantity + qty: cumulativeQty }); if (submitQty > 0) { @@ -1281,7 +1337,7 @@ const JobPickExecution: React.FC = ({ 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...`); @@ -1910,13 +1966,24 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { // 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 + uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '', + pickedQty: selectedLotForExecutionForm.actualPickQty || 0, + suggestedList: [], + noLotLines: [] }} pickOrderId={selectedLotForExecutionForm.pickOrderId} pickOrderCreateDate={new Date()} + onNormalPickSubmit={async (lot, submitQty) => { + console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty }); + if (!lot) { + console.error('Lot is null or undefined'); + return; + } + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + handlePickQtyChange(lotKey, submitQty); + await handleSubmitPickQtyWithQty(lot, submitQty); + }} /> )} diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index f05a8b0..15007b3 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -44,6 +44,9 @@ interface LotPickData { stockOutLineId?: number; stockOutLineStatus?: string; stockOutLineQty?: number; + pickOrderLineId?: number; + pickOrderId?: number; + pickOrderCode?: string; } interface PickExecutionFormProps { @@ -54,6 +57,7 @@ interface PickExecutionFormProps { selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; pickOrderId?: number; pickOrderCreateDate: any; + onNormalPickSubmit?: (lot: LotPickData, submitQty: number) => Promise; // Remove these props since we're not handling normal cases // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise; // selectedRowId?: number | null; @@ -76,9 +80,8 @@ const PickExecutionForm: React.FC = ({ selectedPickOrderLine, pickOrderId, pickOrderCreateDate, - // Remove these props - // onNormalPickSubmit, - // selectedRowId, + onNormalPickSubmit, + }) => { const { t } = useTranslation(); const [formData, setFormData] = useState>({}); @@ -87,6 +90,7 @@ const PickExecutionForm: React.FC = ({ const [handlers, setHandlers] = useState>([]); const [verifiedQty, setVerifiedQty] = useState(0); const { data: session } = useSession() as { data: SessionWithTokens | null }; + const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { return lot.availableQty || 0; }, []); @@ -95,7 +99,15 @@ const PickExecutionForm: React.FC = ({ // The actualPickQty in the form should be independent of the database value return lot.requiredQty || 0; }, []); - + useEffect(() => { + console.log('PickExecutionForm props:', { + open, + onNormalPickSubmit: typeof onNormalPickSubmit, + hasOnNormalPickSubmit: !!onNormalPickSubmit, + onSubmit: typeof onSubmit, + }); + }, [open, onNormalPickSubmit, onSubmit]); + // 获取处理人员列表 useEffect(() => { const fetchHandlers = async () => { @@ -184,36 +196,52 @@ useEffect(() => { if (verifiedQty === undefined || verifiedQty < 0) { newErrors.actualPickQty = t('Qty is required'); } - - // 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0 - // if (verifiedQty > receivedQty) { ... } ← 删除 - - // 只检查总和是否等于需求数量 + const totalQty = verifiedQty + badItemQty + missQty; - if (totalQty !== requiredQty) { + const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0; + + if (hasAnyValue && totalQty !== requiredQty) { newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity'); } - // Require either missQty > 0 OR badItemQty > 0 - const hasMissQty = formData.missQty && formData.missQty > 0; - const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; - - if (!hasMissQty && !hasBadItemQty) { - newErrors.missQty = t('At least one issue must be reported'); - newErrors.badItemQty = t('At least one issue must be reported'); - } + setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { + if (!formData.pickOrderId || !selectedLot) { + return; + } + + // Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0) + const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0)) + && formData.missQty == 0 && formData.badItemQty == 0; + + if (isNormalPick) { + if (onNormalPickSubmit) { + setLoading(true); + try { + console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty }); + await onNormalPickSubmit(selectedLot, verifiedQty); + onClose(); + } catch (error) { + console.error('Error submitting normal pick:', error); + } finally { + setLoading(false); + } + } else { + console.warn('onNormalPickSubmit callback not provided'); + } + return; + } + if (!validateForm() || !formData.pickOrderId) { return; } setLoading(true); try { - // Use the verified quantity in the submission const submissionData = { ...formData, actualPickQty: verifiedQty, diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index 0d7b1b4..b68616f 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -387,7 +387,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; }, [pickOrders, t, tabIndex, items], ); - + const handleSwitchToRecordTab = useCallback(() => { + setTabIndex(1); // 切换到 CompleteJobOrderRecord 标签页(tabIndex 1) + }, []); const fetchNewPagePickOrder = useCallback( async ( pagingController: Record, @@ -438,10 +440,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; +{/* Last 2 buttons aligned right - {/* Last 2 buttons aligned right */} - {/* Unassigned Job Orders */} + {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( @@ -463,7 +465,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; )} - +*/} @@ -474,9 +476,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; borderBottom: '1px solid #e0e0e0' }}> - + {/* */} + - {/* */} + {/* */} {/* */} @@ -487,9 +490,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; - {tabIndex === 0 && } + {/* {tabIndex === 0 && } */} {tabIndex === 1 && } - {/* {tabIndex === 2 && } */} + {tabIndex === 0 && } {/* {tabIndex === 2 && } */} {/* {tabIndex === 3 && } */} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 6f7fda2..2ba0e66 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -67,6 +67,7 @@ import FGPickOrderCard from "./FGPickOrderCard"; import LotConfirmationModal from "./LotConfirmationModal"; interface Props { filterArgs: Record; + onSwitchToRecordTab: () => void; } // QR Code Modal Component (from GoodPickExecution) @@ -323,7 +324,7 @@ const QrCodeModal: React.FC<{ ); }; -const JobPickExecution: React.FC = ({ filterArgs }) => { +const JobPickExecution: React.FC = ({ filterArgs, onSwitchToRecordTab }) => { const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -1180,11 +1181,69 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } try { - // FIXED: Calculate cumulative quantity correctly + // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0 + if (submitQty === 0) { + console.log(`=== SUBMITTING ALL ZEROS CASE ===`); + console.log(`Lot: ${lot.lotNo}`); + console.log(`Stock Out Line ID: ${lot.stockOutLineId}`); + console.log(`Setting status to 'completed' with qty: 0`); + + const updateResult = await updateStockOutLineStatus({ + id: lot.stockOutLineId, + status: 'completed', + qty: 0 + }); + + console.log('Update result:', updateResult); + const r: any = updateResult as any; + const updateOk = + r?.code === 'SUCCESS' || + r?.type === 'completed' || + typeof r?.id === 'number' || + typeof r?.entity?.id === 'number' || + (r?.message && r.message.includes('successfully')); + if (!updateResult || !updateOk) { + console.error('Failed to update stock out line status:', updateResult); + throw new Error('Failed to update stock out line status'); + } + + + // Check if pick order is completed + if (lot.pickOrderConsoCode) { + console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`); + + try { + const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); + console.log(` Pick order completion check result:`, completionResponse); + + if (completionResponse.code === "SUCCESS") { + console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`); + } else if (completionResponse.message === "not completed") { + console.log(`⏳ Pick order not completed yet, more lines remaining`); + } else { + console.error(`❌ Error checking completion: ${completionResponse.message}`); + } + } catch (error) { + console.error("Error checking pick order completion:", error); + } + } + + const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; + await fetchJobOrderData(pickOrderId); + console.log("All zeros submission completed successfully!"); + + setTimeout(() => { + checkAndAutoAssignNext(); + }, 1000); + + return; + } + + // Normal case: Calculate cumulative quantity correctly const currentActualPickQty = lot.actualPickQty || 0; const cumulativeQty = currentActualPickQty + submitQty; - // FIXED: Determine status based on cumulative quantity vs required quantity + // Determine status based on cumulative quantity vs required quantity let newStatus = 'partially_completed'; if (cumulativeQty >= lot.requiredQty) { @@ -1207,7 +1266,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { await updateStockOutLineStatus({ id: lot.stockOutLineId, status: newStatus, - qty: cumulativeQty // Use cumulative quantity + qty: cumulativeQty }); if (submitQty > 0) { @@ -1219,7 +1278,7 @@ const JobPickExecution: React.FC = ({ 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...`); @@ -1250,7 +1309,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } catch (error) { console.error("Error submitting pick quantity:", error); } - }, [fetchJobOrderData, checkAndAutoAssignNext]); + }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked' @@ -1306,6 +1365,9 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { setTimeout(() => { setQrScanSuccess(false); checkAndAutoAssignNext(); + if (onSwitchToRecordTab) { + onSwitchToRecordTab(); + } }, 2000); } else { console.error("Batch submit failed:", result); @@ -1318,7 +1380,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { } finally { setIsSubmittingAll(false); } - }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId]) + }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab]) // Calculate scanned items count const scannedItemsCount = useMemo(() => { @@ -1852,13 +1914,24 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { // 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 + uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '', + pickedQty: selectedLotForExecutionForm.actualPickQty || 0, + suggestedList: [], + noLotLines: [] }} pickOrderId={selectedLotForExecutionForm.pickOrderId} pickOrderCreateDate={new Date()} + onNormalPickSubmit={async (lot, submitQty) => { + console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty }); + if (!lot) { + console.error('Lot is null or undefined'); + return; + } + const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`; + handlePickQtyChange(lotKey, submitQty); + await handleSubmitPickQtyWithQty(lot, submitQty); + }} /> )} diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 7f2aef1..01f9e34 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -14,10 +14,16 @@ import { Tabs, Tab, TabsProps, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + InputAdornment } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; -import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions"; +import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; @@ -31,6 +37,7 @@ import { InventoryResult } from "@/app/api/inventory"; import { releaseJo, startJo } from "@/app/api/jo/actions"; import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; import ProcessSummaryHeader from "./ProcessSummaryHeader"; +import EditIcon from "@mui/icons-material/Edit"; interface JobOrderLine { id: number; jobOrderId: number; @@ -64,8 +71,9 @@ const ProductionProcessJobOrderDetail: React.FC([]); const [tabIndex, setTabIndex] = useState(0); const [selectedProcessId, setSelectedProcessId] = useState(null); - - // 获取数据 + const [operationPriority, setOperationPriority] = useState(50); + const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); + const fetchData = useCallback(async () => { setLoading(true); try { @@ -117,7 +125,25 @@ const getStockAvailable = (line: JobOrderLine) => { } return line.stockQty || 0; }; +const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { + const response = await updateProductProcessPriority(productProcessId, productionPriority) + if (response) { + await fetchData(); + } +}, [jobOrderId]); +const handleOpenPriorityDialog = () => { + setOperationPriority(processData?.productionPriority ?? 50); + setOpenOperationPriorityDialog(true); +}; +const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { + setOpenOperationPriorityDialog(false); +}; +const handleConfirmPriority = async () => { + if (!processData?.id) return; + await handleUpdateOperationPriority(processData.id, Number(operationPriority)); + setOpenOperationPriorityDialog(false); +}; const isStockSufficient = (line: JobOrderLine) => { if (line.type?.toLowerCase() === "consumables") { return false; @@ -248,12 +274,21 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { /> - + + + + + + ), + }} + /> { align: "right", headerAlign: "right", renderCell: (params: GridRenderCellParams) => { - if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb" ) { - return t("N/A"); - } + return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`; }, @@ -350,14 +383,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { type: "number", renderCell: (params: GridRenderCellParams) => { // 如果是 consumables,显示 N/A - if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { - return t("N/A"); - } + const stockAvailable = getStockAvailable(params.row); - if (stockAvailable === null) { - return t("N/A"); - } - return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`; + + return `${decimalFormatter.format(stockAvailable || 0)} (${params.row.shortUom})`; }, }, { @@ -386,9 +415,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { headerAlign: "center", type: "boolean", renderCell: (params: GridRenderCellParams) => { - if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { - return {t("N/A")}; - } + return isStockSufficient(params.row) ? : ; @@ -520,11 +547,36 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { {tabIndex === 4 && } + + {t("Update Production Priority")} + + setOperationPriority(Number(e.target.value))} + /> + + + + + + + ); + }; export default ProductionProcessJobOrderDetail; \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index f225dd1..1f0cb0f 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -195,6 +195,7 @@ "Remark": "明細", "Req. Qty": "需求數量", "Seq No": "加入步驟", + "Total pick orders": "總提料單數量", "Seq No Remark": "序號明細", "Stock Available": "庫存可用", "Confirm": "確認", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index b297f35..0266175 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -43,6 +43,7 @@ "Item Code": "成品/半成品編號", "Paused": "已暫停", "paused": "已暫停", + "Total pick orders": "總提料單數量", "Pause Reason": "暫停原因", "Reason": "原因", "Stock Available": "倉庫可用數",