| @@ -683,7 +683,14 @@ export const fetchProductProcessById = cache(async (id: number) => { | |||
| } | |||
| ); | |||
| }); | |||
| export const updateProductProcessPriority = cache(async (productProcessId: number, productionPriority: number) => { | |||
| return serverFetchJson<any>( | |||
| `${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<ProductProcessWithLinesResponse[]>( | |||
| @@ -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<SearchJoResultResponse>( | |||
| 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<SearchJoResultResponse>( | |||
| `${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 | |||
| }) | |||
| @@ -204,6 +204,9 @@ export const fetchPoListClient = cache( | |||
| async (queryParams?: Record<string, any>) => { | |||
| 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<RecordsRes<PoResult[]>>( | |||
| `${BASE_API_URL}/po/list?${queryString}`, | |||
| { | |||
| @@ -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); | |||
| </Alert> | |||
| )} | |||
| {/*{ | |||
| formProps.watch("status")?.toLowerCase() === "pending" && ( | |||
| {formProps.watch("status")?.toLowerCase() === "pending" && ( | |||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -180,9 +184,10 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| {t("Release")} | |||
| </Button> | |||
| </Stack> | |||
| )} | |||
| */} | |||
| ) | |||
| } | |||
| {/* ADD STORE-BASED ASSIGNMENT BUTTONS */} | |||
| {/* | |||
| { | |||
| formProps.watch("status")?.toLowerCase() === "released" && ( | |||
| <Box sx={{ mb: 2 }}> | |||
| @@ -232,7 +237,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||
| </Stack> | |||
| </Box> | |||
| )} | |||
| */} | |||
| <DoInfoCard /> | |||
| <DoLineTable /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| @@ -76,7 +76,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...pagingController, | |||
| @@ -175,6 +175,9 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||
| const onDetailClick = useCallback( | |||
| (doResult: DoResult) => { | |||
| if (typeof window !== 'undefined') { | |||
| sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams)); | |||
| } | |||
| router.push(`/do/edit?id=${doResult.id}`); | |||
| }, | |||
| [router], | |||
| @@ -287,7 +290,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||
| const handleSearch = useCallback(async (query: SearchBoxInputs) => { | |||
| try { | |||
| setCurrentSearchParams(query); | |||
| let orderStartDate = ""; | |||
| let orderEndDate = ""; | |||
| let estArrStartDate = query.estimatedArrivalDate; | |||
| @@ -328,13 +331,48 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||
| setSearchAllDos(data); | |||
| setHasSearched(true); | |||
| setHasResults(data.length > 0); | |||
| } catch (error) { | |||
| console.error("Error: ", error); | |||
| setSearchAllDos([]); | |||
| setHasSearched(true); | |||
| setHasResults(false); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| if (typeof window !== 'undefined') { | |||
| const savedSearchParams = sessionStorage.getItem('doSearchParams'); | |||
| if (savedSearchParams) { | |||
| try { | |||
| const params = JSON.parse(savedSearchParams); | |||
| setCurrentSearchParams(params); | |||
| // 自动使用保存的搜索条件重新搜索,获取最新数据 | |||
| const timer = setTimeout(async () => { | |||
| await handleSearch(params); | |||
| // 搜索完成后,清除 sessionStorage | |||
| if (typeof window !== 'undefined') { | |||
| sessionStorage.removeItem('doSearchParams'); | |||
| sessionStorage.removeItem('doSearchResults'); | |||
| sessionStorage.removeItem('doSearchHasSearched'); | |||
| } | |||
| }, 100); | |||
| return () => clearTimeout(timer); | |||
| } catch (e) { | |||
| console.error('Error restoring search state:', e); | |||
| // 如果出错,也清除 sessionStorage | |||
| if (typeof window !== 'undefined') { | |||
| sessionStorage.removeItem('doSearchParams'); | |||
| sessionStorage.removeItem('doSearchResults'); | |||
| sessionStorage.removeItem('doSearchHasSearched'); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }, [handleSearch]); | |||
| const debouncedSearch = useCallback((query: SearchBoxInputs) => { | |||
| if (searchTimeout) { | |||
| clearTimeout(searchTimeout); | |||
| @@ -27,7 +27,6 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { | |||
| fetchALLPickOrderLineLotDetails, | |||
| updateStockOutLineStatus, | |||
| createStockOutLine, | |||
| updateStockOutLine, | |||
| @@ -634,6 +633,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| const flatLotData: any[] = []; | |||
| mergedPickOrder.pickOrderLines.forEach((line: any) => { | |||
| // ✅ FIXED: 处理 lots(如果有) | |||
| if (line.lots && line.lots.length > 0) { | |||
| // 修复:先对 lots 按 lotId 去重并合并 requiredQty | |||
| const lotMap = new Map<number, any>(); | |||
| @@ -696,53 +696,54 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| noLot: false, | |||
| }); | |||
| }); | |||
| } else { | |||
| // 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id | |||
| const firstStockout = line.stockouts && line.stockouts.length > 0 | |||
| ? line.stockouts[0] | |||
| : null; | |||
| flatLotData.push({ | |||
| pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // 修复:consoCodes 是数组 | |||
| pickOrderTargetDate: mergedPickOrder.targetDate, | |||
| pickOrderStatus: mergedPickOrder.status, | |||
| pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| itemId: line.item.id, | |||
| itemCode: line.item.code, | |||
| itemName: line.item.name, | |||
| uomDesc: line.item.uomDesc, | |||
| uomShortDesc: line.item.uomShortDesc, | |||
| // Null stock 字段 - 从 stockouts 数组中获取 | |||
| lotId: firstStockout?.lotId || null, | |||
| lotNo: firstStockout?.lotNo || null, | |||
| expiryDate: null, | |||
| location: firstStockout?.location || null, | |||
| stockUnit: line.item.uomDesc, | |||
| availableQty: firstStockout?.availableQty || 0, | |||
| requiredQty: line.requiredQty, | |||
| actualPickQty: firstStockout?.qty || 0, | |||
| inQty: 0, | |||
| outQty: 0, | |||
| holdQty: 0, | |||
| lotStatus: 'unavailable', | |||
| lotAvailability: 'insufficient_stock', | |||
| processingStatus: firstStockout?.status || 'pending', | |||
| suggestedPickLotId: null, | |||
| stockOutLineId: firstStockout?.id || null, // 使用 stockouts 数组中的 id | |||
| stockOutLineStatus: firstStockout?.status || null, | |||
| stockOutLineQty: firstStockout?.qty || 0, | |||
| routerId: null, | |||
| routerIndex: 999999, | |||
| routerRoute: null, | |||
| routerArea: null, | |||
| noLot: true, | |||
| } | |||
| // ✅ FIXED: 同时处理 stockouts(无论是否有 lots) | |||
| if (line.stockouts && line.stockouts.length > 0) { | |||
| // ✅ FIXED: 处理所有 stockouts,而不仅仅是第一个 | |||
| line.stockouts.forEach((stockout: any) => { | |||
| flatLotData.push({ | |||
| pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", | |||
| pickOrderTargetDate: mergedPickOrder.targetDate, | |||
| pickOrderStatus: mergedPickOrder.status, | |||
| pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, | |||
| pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "", | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| itemId: line.item.id, | |||
| itemCode: line.item.code, | |||
| itemName: line.item.name, | |||
| uomDesc: line.item.uomDesc, | |||
| uomShortDesc: line.item.uomShortDesc, | |||
| // Null stock 字段 - 从 stockouts 数组中获取 | |||
| lotId: stockout.lotId || null, | |||
| lotNo: stockout.lotNo || null, | |||
| expiryDate: null, | |||
| location: stockout.location || null, | |||
| stockUnit: line.item.uomDesc, | |||
| availableQty: stockout.availableQty || 0, | |||
| requiredQty: line.requiredQty, | |||
| actualPickQty: stockout.qty || 0, | |||
| inQty: 0, | |||
| outQty: 0, | |||
| holdQty: 0, | |||
| lotStatus: 'unavailable', | |||
| lotAvailability: 'insufficient_stock', | |||
| processingStatus: stockout.status || 'pending', | |||
| suggestedPickLotId: null, | |||
| stockOutLineId: stockout.id || null, // 使用 stockouts 数组中的 id | |||
| stockOutLineStatus: stockout.status || null, | |||
| stockOutLineQty: stockout.qty || 0, | |||
| routerId: null, | |||
| routerIndex: 999999, | |||
| routerRoute: null, | |||
| routerArea: null, | |||
| noLot: true, | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| @@ -1815,10 +1816,11 @@ const allItemsReady = useMemo(() => { | |||
| const isCompleted = | |||
| status === 'completed' || status === 'partially_completed' || status === 'partially_complete'; | |||
| const isChecked = status === 'checked'; | |||
| const isPending = status === 'pending'; | |||
| // 无库存(noLot)行:只要状态不是 pending/rejected 即视为已处理 | |||
| // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交) | |||
| if (lot.noLot === true) { | |||
| return isChecked || isCompleted || isRejected; | |||
| return isChecked || isCompleted || isRejected || isPending; | |||
| } | |||
| // 正常 lot:必须已扫描/提交或者被拒收 | |||
| @@ -2105,14 +2107,13 @@ const handleSubmitAllScanned = useCallback(async () => { | |||
| // Calculate scanned items count (should match handleSubmitAllScanned filter logic) | |||
| const scannedItemsCount = useMemo(() => { | |||
| const filtered = combinedLotData.filter(lot => { | |||
| // 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含 | |||
| // ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑 | |||
| if (lot.noLot === true) { | |||
| const status = lot.stockOutLineStatus?.toLowerCase(); | |||
| const include = status !== 'completed' && status !== 'rejected'; | |||
| if (include) { | |||
| console.log(`📊 Including noLot item: ${lot.itemName || lot.itemCode}, status: ${lot.stockOutLineStatus}`); | |||
| } | |||
| return include; | |||
| // ✅ 只包含可以提交的状态(与 handleSubmitAllScanned 保持一致) | |||
| return lot.stockOutLineStatus === 'checked' || | |||
| lot.stockOutLineStatus === 'pending' || | |||
| lot.stockOutLineStatus === 'partially_completed' || | |||
| lot.stockOutLineStatus === 'PARTIALLY_COMPLETE'; | |||
| } | |||
| // 正常情况:只包含 checked 状态 | |||
| return lot.stockOutLineStatus === 'checked'; | |||
| @@ -2601,6 +2602,14 @@ paginatedData.map((lot, index) => { | |||
| setLotConfirmationOpen(false); | |||
| setExpectedLotData(null); | |||
| setScannedLotData(null); | |||
| if (lastProcessedQr) { | |||
| setProcessedQrCodes(prev => { | |||
| const newSet = new Set(prev); | |||
| newSet.delete(lastProcessedQr); | |||
| return newSet; | |||
| }); | |||
| setLastProcessedQr(''); | |||
| } | |||
| }} | |||
| onConfirm={handleLotConfirmation} | |||
| expectedLot={expectedLotData} | |||
| @@ -174,6 +174,10 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||
| error={Boolean(error)} | |||
| variant="outlined" | |||
| type="number" | |||
| disabled={true} | |||
| // sx={{ | |||
| // backgroundColor: "background.paper", | |||
| // }} | |||
| value={field.value ?? ""} | |||
| onChange={(e) => { | |||
| const val = e.target.value === "" ? undefined : Number(e.target.value); | |||
| @@ -22,11 +22,11 @@ import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { createStockInLine } from "@/app/api/stockIn/actions"; | |||
| import { msg } from "../Swal/CustomAlerts"; | |||
| import dayjs from "dayjs"; | |||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||
| interface Props { | |||
| defaultInputs: SearchJoResultRequest, | |||
| bomCombo: BomCombo[] | |||
| @@ -35,7 +35,6 @@ interface Props { | |||
| } | |||
| type SearchQuery = Partial<Omit<JobOrder, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => { | |||
| @@ -49,9 +48,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | |||
| // console.log(inputs) | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | |||
| const fetchJoDetailClient = async (id: number): Promise<JobOrder> => { | |||
| @@ -68,7 +65,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| for (const jo of filteredJos) { | |||
| try { | |||
| const detailedJo = await fetchJoDetailClient(jo.id); // Use client function | |||
| const detailedJo = await fetchJoDetailClient(jo.id); | |||
| detailedMap.set(jo.id, detailedJo); | |||
| } catch (error) { | |||
| console.error(`Error fetching detail for JO ${jo.id}:`, error); | |||
| @@ -84,20 +81,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| }, [filteredJos]); | |||
| useEffect(() => { | |||
| const fetchInventoryData = async () => { | |||
| try { | |||
| const inventoryResponse = await fetchInventories({ | |||
| code: "", | |||
| name: "", | |||
| type: "", | |||
| pageNum: 0, | |||
| pageSize: 1000 | |||
| }); | |||
| setInventoryData(inventoryResponse.records); | |||
| } catch (error) { | |||
| console.error("Error fetching inventory data:", error); | |||
| } | |||
| }; | |||
| const fetchInventoryData = async () => { | |||
| try { | |||
| const inventoryResponse = await fetchInventories({ | |||
| code: "", | |||
| name: "", | |||
| type: "", | |||
| pageNum: 0, | |||
| pageSize: 1000 | |||
| }); | |||
| setInventoryData(inventoryResponse.records); | |||
| } catch (error) { | |||
| console.error("Error fetching inventory data:", error); | |||
| } | |||
| }; | |||
| fetchInventoryData(); | |||
| }, []); | |||
| @@ -120,7 +117,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| }; | |||
| const getStockCounts = (jo: JobOrder) => { | |||
| return { | |||
| sufficient: jo.sufficientCount, | |||
| insufficient: jo.insufficientCount | |||
| @@ -140,7 +136,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| type: "select", | |||
| options: jobTypes.map(jt => jt.name) | |||
| }, | |||
| ], [t]) | |||
| ], [t, jobTypes]) | |||
| const columns = useMemo<Column<JobOrder>[]>( | |||
| () => [ | |||
| @@ -177,7 +173,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (row) => { // TODO improve | |||
| renderCell: (row) => { | |||
| return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}> | |||
| {t(upperFirst(row.status))} | |||
| </span> | |||
| @@ -213,36 +209,62 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| } | |||
| }, | |||
| { | |||
| // TODO put it inside Action Buttons | |||
| name: "id", | |||
| label: t("Actions"), | |||
| // onClick: (record) => onDetailClick(record), | |||
| // buttonIcon: <EditNote />, | |||
| renderCell: (row) => { | |||
| //const btnSx = getButtonSx(row); | |||
| return ( | |||
| <Button | |||
| id="emailSupplier" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| // sx={{ width: "150px", backgroundColor: btnSx.color }} | |||
| sx={{ width: "150px" }} | |||
| // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"} | |||
| onClick={() => onDetailClick(row)} | |||
| // >{btnSx.label} | |||
| >{t("View")} | |||
| id="emailSupplier" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ width: "150px" }} | |||
| onClick={() => onDetailClick(row)} | |||
| > | |||
| {t("View")} | |||
| </Button> | |||
| ) | |||
| } | |||
| }, | |||
| ], [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<Props> = ({ 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<SearchParamNames, string> | 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<Props> = ({ 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<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| const [openModal, setOpenModal] = useState<boolean>(false); | |||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | |||
| /* | |||
| 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<SearchParamNames, string>) => { | |||
| 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<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| onSearch={onSearch} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<JobOrder> | |||
| <SearchResults<JobOrder> | |||
| items={filteredJos} | |||
| columns={columns} | |||
| setPagingController={setPagingController} | |||
| pagingController={pagingController} | |||
| totalCount={totalCount} | |||
| // isAutoPaging={false} | |||
| isAutoPaging={false} | |||
| /> | |||
| <JoCreateFormModal | |||
| open={isCreateJoModalOpen} | |||
| bomCombo={bomCombo} | |||
| onClose={onCloseCreateJoModal} | |||
| onSearch={searchDataByPage} | |||
| onSearch={() => { | |||
| }} | |||
| /> | |||
| <QcStockInModal | |||
| @@ -446,7 +410,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| onClose={closeNewModal} | |||
| inputDetail={modalInfo} | |||
| printerCombo={printerCombo} | |||
| // skipQc={true} | |||
| /> | |||
| </> | |||
| } | |||
| @@ -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<Props> = ({ onSwitchToRecordTab }) =>{ | |||
| const { t } = useTranslation(["common", "jo"]); | |||
| const [loading, setLoading] = useState(false); | |||
| const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | |||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | |||
| const fetchPickOrders = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| @@ -62,7 +64,7 @@ const JoPickOrderList: React.FC = () => { | |||
| {t("Back to List")} | |||
| </Button> | |||
| </Box> | |||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} /> | |||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} /> | |||
| </Box> | |||
| ); | |||
| } | |||
| @@ -1236,17 +1236,73 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<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...`); | |||
| @@ -1910,13 +1966,24 @@ const JobPickExecution: React.FC<Props> = ({ 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); | |||
| }} | |||
| /> | |||
| )} | |||
| </FormProvider> | |||
| @@ -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<void>; | |||
| // Remove these props since we're not handling normal cases | |||
| // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | |||
| // selectedRowId?: number | null; | |||
| @@ -76,9 +80,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| selectedPickOrderLine, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| // Remove these props | |||
| // onNormalPickSubmit, | |||
| // selectedRowId, | |||
| onNormalPickSubmit, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | |||
| @@ -87,6 +90,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | |||
| const [verifiedQty, setVerifiedQty] = useState<number>(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<PickExecutionFormProps> = ({ | |||
| // 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, | |||
| @@ -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<string, number>, | |||
| @@ -438,10 +440,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| <Grid item xs={8}> | |||
| </Grid> | |||
| {/* Last 2 buttons aligned right | |||
| {/* Last 2 buttons aligned right */} | |||
| <Grid item xs={6} > | |||
| {/* Unassigned Job Orders */} | |||
| {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( | |||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | |||
| <Typography variant="h6" gutterBottom> | |||
| @@ -463,7 +465,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| </Box> | |||
| )} | |||
| </Grid> | |||
| */} | |||
| </Grid> | |||
| </Stack> | |||
| @@ -474,9 +476,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| borderBottom: '1px solid #e0e0e0' | |||
| }}> | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Pick Order Detail")} iconPosition="end" /> | |||
| {/* <Tab label={t("Pick Order Detail")} iconPosition="end" /> */} | |||
| <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Complete Job Order Record")} iconPosition="end" /> | |||
| {/* <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> */} | |||
| {/* <Tab label={t("Job Order Match")} iconPosition="end" /> */} | |||
| {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | |||
| </Tabs> | |||
| @@ -487,9 +490,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||
| <Box sx={{ | |||
| p: 2 | |||
| }}> | |||
| {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | |||
| {/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */} | |||
| {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | |||
| {/* {tabIndex === 2 && <JoPickOrderList />} */} | |||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||
| {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | |||
| {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | |||
| </Box> | |||
| @@ -67,6 +67,7 @@ import FGPickOrderCard from "./FGPickOrderCard"; | |||
| import LotConfirmationModal from "./LotConfirmationModal"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| onSwitchToRecordTab: () => void; | |||
| } | |||
| // QR Code Modal Component (from GoodPickExecution) | |||
| @@ -323,7 +324,7 @@ const QrCodeModal: React.FC<{ | |||
| ); | |||
| }; | |||
| const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<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...`); | |||
| @@ -1250,7 +1309,7 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ filterArgs }) => { | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| checkAndAutoAssignNext(); | |||
| if (onSwitchToRecordTab) { | |||
| onSwitchToRecordTab(); | |||
| } | |||
| }, 2000); | |||
| } else { | |||
| console.error("Batch submit failed:", result); | |||
| @@ -1318,7 +1380,7 @@ const JobPickExecution: React.FC<Props> = ({ 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<Props> = ({ 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); | |||
| }} | |||
| /> | |||
| )} | |||
| </FormProvider> | |||
| @@ -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<ProductProcessJobOrderDetailProp | |||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | |||
| // 获取数据 | |||
| const [operationPriority, setOperationPriority] = useState<number>(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) => { | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Production Priority")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.productionPriority ||processData?.isDense === 0 ? "50" : processData?.productionPriority || "0"} | |||
| /> | |||
| <TextField | |||
| label={t("Production Priority")} | |||
| fullWidth | |||
| disabled={true} | |||
| value={processData?.productionPriority ?? "50"} | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <InputAdornment position="end"> | |||
| <IconButton size="small" onClick={handleOpenPriorityDialog}> | |||
| <EditIcon fontSize="small" /> | |||
| </IconButton> | |||
| </InputAdornment> | |||
| ), | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| @@ -334,9 +369,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| 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<JobOrderLine>) => { | |||
| // 如果是 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<JobOrderLine>) => { | |||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||
| return <Typography>{t("N/A")}</Typography>; | |||
| } | |||
| return isStockSufficient(params.row) | |||
| ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | |||
| : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />; | |||
| @@ -520,11 +547,36 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} | |||
| <Dialog | |||
| open={openOperationPriorityDialog} | |||
| onClose={handleClosePriorityDialog} | |||
| fullWidth | |||
| maxWidth="xs" | |||
| > | |||
| <DialogTitle>{t("Update Production Priority")}</DialogTitle> | |||
| <DialogContent> | |||
| <TextField | |||
| autoFocus | |||
| margin="dense" | |||
| label={t("Production Priority")} | |||
| type="number" | |||
| fullWidth | |||
| value={operationPriority} | |||
| onChange={(e) => setOperationPriority(Number(e.target.value))} | |||
| /> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button> | |||
| <Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default ProductionProcessJobOrderDetail; | |||
| @@ -195,6 +195,7 @@ | |||
| "Remark": "明細", | |||
| "Req. Qty": "需求數量", | |||
| "Seq No": "加入步驟", | |||
| "Total pick orders": "總提料單數量", | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Confirm": "確認", | |||
| @@ -43,6 +43,7 @@ | |||
| "Item Code": "成品/半成品編號", | |||
| "Paused": "已暫停", | |||
| "paused": "已暫停", | |||
| "Total pick orders": "總提料單數量", | |||
| "Pause Reason": "暫停原因", | |||
| "Reason": "原因", | |||
| "Stock Available": "倉庫可用數", | |||