| @@ -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 查询 | // 根据 Job Order ID 查询 | ||||
| export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number) => { | export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number) => { | ||||
| return serverFetchJson<ProductProcessWithLinesResponse[]>( | return serverFetchJson<ProductProcessWithLinesResponse[]>( | ||||
| @@ -879,7 +886,10 @@ export const isCorrectMachineUsed = async (machineCode: string) => { | |||||
| export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | ||||
| const queryStr = convertObjToURLSearchParams(data) | const queryStr = convertObjToURLSearchParams(data) | ||||
| console.log("queryStr", queryStr) | 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}`, | `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -889,7 +899,8 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | |||||
| } | } | ||||
| } | } | ||||
| ) | ) | ||||
| console.log("fetchJos response:", response) | |||||
| return response | return response | ||||
| }) | }) | ||||
| @@ -204,6 +204,9 @@ export const fetchPoListClient = cache( | |||||
| async (queryParams?: Record<string, any>) => { | async (queryParams?: Record<string, any>) => { | ||||
| if (queryParams) { | if (queryParams) { | ||||
| const queryString = new URLSearchParams(queryParams).toString(); | 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[]>>( | return serverFetchJson<RecordsRes<PoResult[]>>( | ||||
| `${BASE_API_URL}/po/list?${queryString}`, | `${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 { Button, Stack, Typography, Box, Alert } from "@mui/material"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import StartIcon from "@mui/icons-material/Start"; | 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 DoInfoCard from "./DoInfoCard"; | ||||
| import DoLineTable from "./DoLineTable"; | import DoLineTable from "./DoLineTable"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| @@ -41,7 +41,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||||
| const handleBack = useCallback(() => { | const handleBack = useCallback(() => { | ||||
| router.replace(`/do`) | router.replace(`/do`) | ||||
| }, []) | |||||
| }, [router]) | |||||
| const handleRelease = useCallback(async () => { | const handleRelease = useCallback(async () => { | ||||
| try { | try { | ||||
| @@ -57,12 +57,16 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||||
| // setServerError("User session not found. Please login again."); | // setServerError("User session not found. Please login again."); | ||||
| // return; | // return; | ||||
| //} | //} | ||||
| /* | |||||
| const response = await releaseDo({ | const response = await releaseDo({ | ||||
| id: id, | id: id, | ||||
| //userId: currentUserId // Pass user ID from session | //userId: currentUserId // Pass user ID from session | ||||
| }) | }) | ||||
| */ | |||||
| const response = await startBatchReleaseAsyncSingle({ | |||||
| doId: id, | |||||
| userId: currentUserId ?? 0 | |||||
| }) | |||||
| if (response) { | if (response) { | ||||
| formProps.setValue("status", response.entity.status) | formProps.setValue("status", response.entity.status) | ||||
| setSuccessMessage(t("DO released successfully! Pick orders created.")) | setSuccessMessage(t("DO released successfully! Pick orders created.")) | ||||
| @@ -168,8 +172,8 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||||
| </Alert> | </Alert> | ||||
| )} | )} | ||||
| {/*{ | |||||
| formProps.watch("status")?.toLowerCase() === "pending" && ( | |||||
| {formProps.watch("status")?.toLowerCase() === "pending" && ( | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -180,9 +184,10 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||||
| {t("Release")} | {t("Release")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| )} | |||||
| */} | |||||
| ) | |||||
| } | |||||
| {/* ADD STORE-BASED ASSIGNMENT BUTTONS */} | {/* ADD STORE-BASED ASSIGNMENT BUTTONS */} | ||||
| {/* | |||||
| { | { | ||||
| formProps.watch("status")?.toLowerCase() === "released" && ( | formProps.watch("status")?.toLowerCase() === "released" && ( | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| @@ -232,7 +237,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); | |||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| */} | |||||
| <DoInfoCard /> | <DoInfoCard /> | ||||
| <DoLineTable /> | <DoLineTable /> | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| @@ -76,7 +76,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||||
| pageNum: 1, | pageNum: 1, | ||||
| pageSize: 10, | pageSize: 10, | ||||
| }); | }); | ||||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| const newPagingController = { | const newPagingController = { | ||||
| ...pagingController, | ...pagingController, | ||||
| @@ -175,6 +175,9 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (doResult: DoResult) => { | (doResult: DoResult) => { | ||||
| if (typeof window !== 'undefined') { | |||||
| sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams)); | |||||
| } | |||||
| router.push(`/do/edit?id=${doResult.id}`); | router.push(`/do/edit?id=${doResult.id}`); | ||||
| }, | }, | ||||
| [router], | [router], | ||||
| @@ -287,7 +290,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||||
| const handleSearch = useCallback(async (query: SearchBoxInputs) => { | const handleSearch = useCallback(async (query: SearchBoxInputs) => { | ||||
| try { | try { | ||||
| setCurrentSearchParams(query); | setCurrentSearchParams(query); | ||||
| let orderStartDate = ""; | let orderStartDate = ""; | ||||
| let orderEndDate = ""; | let orderEndDate = ""; | ||||
| let estArrStartDate = query.estimatedArrivalDate; | let estArrStartDate = query.estimatedArrivalDate; | ||||
| @@ -328,13 +331,48 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear | |||||
| setSearchAllDos(data); | setSearchAllDos(data); | ||||
| setHasSearched(true); | setHasSearched(true); | ||||
| setHasResults(data.length > 0); | setHasResults(data.length > 0); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error: ", error); | console.error("Error: ", error); | ||||
| setSearchAllDos([]); | setSearchAllDos([]); | ||||
| setHasSearched(true); | setHasSearched(true); | ||||
| setHasResults(false); | 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) => { | const debouncedSearch = useCallback((query: SearchBoxInputs) => { | ||||
| if (searchTimeout) { | if (searchTimeout) { | ||||
| clearTimeout(searchTimeout); | clearTimeout(searchTimeout); | ||||
| @@ -27,7 +27,6 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
| import { | import { | ||||
| fetchALLPickOrderLineLotDetails, | |||||
| updateStockOutLineStatus, | updateStockOutLineStatus, | ||||
| createStockOutLine, | createStockOutLine, | ||||
| updateStockOutLine, | updateStockOutLine, | ||||
| @@ -634,6 +633,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| const flatLotData: any[] = []; | const flatLotData: any[] = []; | ||||
| mergedPickOrder.pickOrderLines.forEach((line: any) => { | mergedPickOrder.pickOrderLines.forEach((line: any) => { | ||||
| // ✅ FIXED: 处理 lots(如果有) | |||||
| if (line.lots && line.lots.length > 0) { | if (line.lots && line.lots.length > 0) { | ||||
| // 修复:先对 lots 按 lotId 去重并合并 requiredQty | // 修复:先对 lots 按 lotId 去重并合并 requiredQty | ||||
| const lotMap = new Map<number, any>(); | const lotMap = new Map<number, any>(); | ||||
| @@ -696,53 +696,54 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| noLot: false, | 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 = | const isCompleted = | ||||
| status === 'completed' || status === 'partially_completed' || status === 'partially_complete'; | status === 'completed' || status === 'partially_completed' || status === 'partially_complete'; | ||||
| const isChecked = status === 'checked'; | const isChecked = status === 'checked'; | ||||
| const isPending = status === 'pending'; | |||||
| // 无库存(noLot)行:只要状态不是 pending/rejected 即视为已处理 | |||||
| // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交) | |||||
| if (lot.noLot === true) { | if (lot.noLot === true) { | ||||
| return isChecked || isCompleted || isRejected; | |||||
| return isChecked || isCompleted || isRejected || isPending; | |||||
| } | } | ||||
| // 正常 lot:必须已扫描/提交或者被拒收 | // 正常 lot:必须已扫描/提交或者被拒收 | ||||
| @@ -2105,14 +2107,13 @@ const handleSubmitAllScanned = useCallback(async () => { | |||||
| // Calculate scanned items count (should match handleSubmitAllScanned filter logic) | // Calculate scanned items count (should match handleSubmitAllScanned filter logic) | ||||
| const scannedItemsCount = useMemo(() => { | const scannedItemsCount = useMemo(() => { | ||||
| const filtered = combinedLotData.filter(lot => { | const filtered = combinedLotData.filter(lot => { | ||||
| // 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含 | |||||
| // ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑 | |||||
| if (lot.noLot === true) { | 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 状态 | // 正常情况:只包含 checked 状态 | ||||
| return lot.stockOutLineStatus === 'checked'; | return lot.stockOutLineStatus === 'checked'; | ||||
| @@ -2601,6 +2602,14 @@ paginatedData.map((lot, index) => { | |||||
| setLotConfirmationOpen(false); | setLotConfirmationOpen(false); | ||||
| setExpectedLotData(null); | setExpectedLotData(null); | ||||
| setScannedLotData(null); | setScannedLotData(null); | ||||
| if (lastProcessedQr) { | |||||
| setProcessedQrCodes(prev => { | |||||
| const newSet = new Set(prev); | |||||
| newSet.delete(lastProcessedQr); | |||||
| return newSet; | |||||
| }); | |||||
| setLastProcessedQr(''); | |||||
| } | |||||
| }} | }} | ||||
| onConfirm={handleLotConfirmation} | onConfirm={handleLotConfirmation} | ||||
| expectedLot={expectedLotData} | expectedLot={expectedLotData} | ||||
| @@ -174,6 +174,10 @@ const JoCreateFormModal: React.FC<Props> = ({ | |||||
| error={Boolean(error)} | error={Boolean(error)} | ||||
| variant="outlined" | variant="outlined" | ||||
| type="number" | type="number" | ||||
| disabled={true} | |||||
| // sx={{ | |||||
| // backgroundColor: "background.paper", | |||||
| // }} | |||||
| value={field.value ?? ""} | value={field.value ?? ""} | ||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const val = e.target.value === "" ? undefined : Number(e.target.value); | 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 { createStockInLine } from "@/app/api/stockIn/actions"; | ||||
| import { msg } from "../Swal/CustomAlerts"; | import { msg } from "../Swal/CustomAlerts"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | import { fetchInventories } from "@/app/api/inventory/actions"; | ||||
| import { InventoryResult } from "@/app/api/inventory"; | import { InventoryResult } from "@/app/api/inventory"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | import { JobTypeResponse } from "@/app/api/jo/actions"; | ||||
| interface Props { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest, | defaultInputs: SearchJoResultRequest, | ||||
| bomCombo: BomCombo[] | bomCombo: BomCombo[] | ||||
| @@ -35,7 +35,6 @@ interface Props { | |||||
| } | } | ||||
| type SearchQuery = Partial<Omit<JobOrder, "id">>; | type SearchQuery = Partial<Omit<JobOrder, "id">>; | ||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => { | 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 [totalCount, setTotalCount] = useState(0) | ||||
| const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) | ||||
| // console.log(inputs) | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | ||||
| const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); | ||||
| const fetchJoDetailClient = async (id: number): Promise<JobOrder> => { | 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) { | for (const jo of filteredJos) { | ||||
| try { | try { | ||||
| const detailedJo = await fetchJoDetailClient(jo.id); // Use client function | |||||
| const detailedJo = await fetchJoDetailClient(jo.id); | |||||
| detailedMap.set(jo.id, detailedJo); | detailedMap.set(jo.id, detailedJo); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error(`Error fetching detail for JO ${jo.id}:`, 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]); | }, [filteredJos]); | ||||
| useEffect(() => { | 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(); | fetchInventoryData(); | ||||
| }, []); | }, []); | ||||
| @@ -120,7 +117,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| }; | }; | ||||
| const getStockCounts = (jo: JobOrder) => { | const getStockCounts = (jo: JobOrder) => { | ||||
| return { | return { | ||||
| sufficient: jo.sufficientCount, | sufficient: jo.sufficientCount, | ||||
| insufficient: jo.insufficientCount | insufficient: jo.insufficientCount | ||||
| @@ -140,7 +136,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| type: "select", | type: "select", | ||||
| options: jobTypes.map(jt => jt.name) | options: jobTypes.map(jt => jt.name) | ||||
| }, | }, | ||||
| ], [t]) | |||||
| ], [t, jobTypes]) | |||||
| const columns = useMemo<Column<JobOrder>[]>( | const columns = useMemo<Column<JobOrder>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -177,7 +173,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| { | { | ||||
| name: "status", | name: "status", | ||||
| label: t("Status"), | label: t("Status"), | ||||
| renderCell: (row) => { // TODO improve | |||||
| renderCell: (row) => { | |||||
| return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}> | return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}> | ||||
| {t(upperFirst(row.status))} | {t(upperFirst(row.status))} | ||||
| </span> | </span> | ||||
| @@ -213,36 +209,62 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| } | } | ||||
| }, | }, | ||||
| { | { | ||||
| // TODO put it inside Action Buttons | |||||
| name: "id", | name: "id", | ||||
| label: t("Actions"), | label: t("Actions"), | ||||
| // onClick: (record) => onDetailClick(record), | |||||
| // buttonIcon: <EditNote />, | |||||
| renderCell: (row) => { | renderCell: (row) => { | ||||
| //const btnSx = getButtonSx(row); | |||||
| return ( | return ( | ||||
| <Button | <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> | </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) => { | const handleUpdate = useCallback(async (jo: JobOrder) => { | ||||
| console.log(jo); | console.log(jo); | ||||
| try { | try { | ||||
| // setIsUploading(true) | |||||
| if (jo.id) { | if (jo.id) { | ||||
| const response = await updateJo({ id: jo.id, status: "storing" }); | const response = await updateJo({ id: jo.id, status: "storing" }); | ||||
| console.log(`%c Updated JO:`, "color:lime", response); | console.log(`%c Updated JO:`, "color:lime", response); | ||||
| @@ -252,64 +274,22 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| productLotNo: jo?.code, | productLotNo: jo?.code, | ||||
| productionDate: arrayToDateString(dayjs(), "input"), | productionDate: arrayToDateString(dayjs(), "input"), | ||||
| jobOrderId: jo?.id, | jobOrderId: jo?.id, | ||||
| // acceptedQty: secondReceiveQty || 0, | |||||
| // acceptedQty: row.acceptedQty, | |||||
| }; | }; | ||||
| const res = await createStockInLine(postData); | const res = await createStockInLine(postData); | ||||
| console.log(`%c Created Stock In Line`, "color:lime", res); | console.log(`%c Created Stock In Line`, "color:lime", res); | ||||
| msg(t("update success")); | msg(t("update success")); | ||||
| refetchData(defaultInputs, "search"); | |||||
| // 重置为默认输入,让 useEffect 自动触发 | |||||
| setInputs(defaultInputs); | |||||
| setPagingController(defaultPagingController); | |||||
| } | } | ||||
| } catch (e) { | } catch (e) { | ||||
| // backend error | |||||
| // setServerError(t("An error has occurred. Please try again later.")); | |||||
| console.log(e); | console.log(e); | ||||
| } finally { | } finally { | ||||
| // setIsUploading(false) | // 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 joStatus = jo.status?.toLowerCase(); | ||||
| const silStatus = jo.stockInLineStatus?.toLowerCase(); | const silStatus = jo.stockInLineStatus?.toLowerCase(); | ||||
| let btnSx = {label:"", color:""}; | 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 "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break; | ||||
| case "pending": btnSx = {label: t("scan picked material"), color:"error.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 "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": | case "storing": | ||||
| switch (silStatus) { | switch (silStatus) { | ||||
| case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break; | 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 [openModal, setOpenModal] = useState<boolean>(false); | ||||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | 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) => { | const onDetailClick = useCallback((record: JobOrder) => { | ||||
| router.push(`/jo/edit?id=${record.id}`) | 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 onSearch = useCallback((query: Record<SearchParamNames, string>) => { | ||||
| const transformedQuery = { | const transformedQuery = { | ||||
| ...query, | ...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, | planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo, | ||||
| jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" | jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" | ||||
| }; | }; | ||||
| setInputs(() => ({ | |||||
| setInputs({ | |||||
| code: transformedQuery.code, | code: transformedQuery.code, | ||||
| itemName: transformedQuery.itemName, | itemName: transformedQuery.itemName, | ||||
| planStart: transformedQuery.planStart, | planStart: transformedQuery.planStart, | ||||
| planStartTo: transformedQuery.planStartTo, | planStartTo: transformedQuery.planStartTo, | ||||
| jobTypeName: transformedQuery.jobTypeName | jobTypeName: transformedQuery.jobTypeName | ||||
| })) | |||||
| refetchData(transformedQuery, "search"); | |||||
| }, []) | |||||
| }); | |||||
| setPagingController(defaultPagingController); | |||||
| }, [defaultInputs]) | |||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| refetchData(defaultInputs, "paging"); | |||||
| }, []) | |||||
| setInputs(defaultInputs); | |||||
| setPagingController(defaultPagingController); | |||||
| }, [defaultInputs]) | |||||
| // Manual Create Jo Related | |||||
| const onOpenCreateJoModal = useCallback(() => { | const onOpenCreateJoModal = useCallback(() => { | ||||
| setIsCreateJoModalOpen(() => true) | setIsCreateJoModalOpen(() => true) | ||||
| @@ -425,19 +387,21 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| onSearch={onSearch} | onSearch={onSearch} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| <SearchResults<JobOrder> | |||||
| <SearchResults<JobOrder> | |||||
| items={filteredJos} | items={filteredJos} | ||||
| columns={columns} | columns={columns} | ||||
| setPagingController={setPagingController} | setPagingController={setPagingController} | ||||
| pagingController={pagingController} | pagingController={pagingController} | ||||
| totalCount={totalCount} | totalCount={totalCount} | ||||
| // isAutoPaging={false} | |||||
| isAutoPaging={false} | |||||
| /> | /> | ||||
| <JoCreateFormModal | <JoCreateFormModal | ||||
| open={isCreateJoModalOpen} | open={isCreateJoModalOpen} | ||||
| bomCombo={bomCombo} | bomCombo={bomCombo} | ||||
| onClose={onCloseCreateJoModal} | onClose={onCloseCreateJoModal} | ||||
| onSearch={searchDataByPage} | |||||
| onSearch={() => { | |||||
| }} | |||||
| /> | /> | ||||
| <QcStockInModal | <QcStockInModal | ||||
| @@ -446,7 +410,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| onClose={closeNewModal} | onClose={closeNewModal} | ||||
| inputDetail={modalInfo} | inputDetail={modalInfo} | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| // skipQc={true} | |||||
| /> | /> | ||||
| </> | </> | ||||
| } | } | ||||
| @@ -17,17 +17,19 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions"; | import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions"; | ||||
| import JobPickExecution from "./newJobPickExecution"; | import JobPickExecution from "./newJobPickExecution"; | ||||
| interface Props { | |||||
| onSwitchToRecordTab?: () => void; | |||||
| } | |||||
| const PER_PAGE = 6; | const PER_PAGE = 6; | ||||
| const JoPickOrderList: React.FC = () => { | |||||
| const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{ | |||||
| const { t } = useTranslation(["common", "jo"]); | const { t } = useTranslation(["common", "jo"]); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); | ||||
| const [page, setPage] = useState(0); | const [page, setPage] = useState(0); | ||||
| const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); | ||||
| const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); | ||||
| const fetchPickOrders = useCallback(async () => { | const fetchPickOrders = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| @@ -62,7 +64,7 @@ const JoPickOrderList: React.FC = () => { | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} /> | |||||
| <JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} /> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1236,17 +1236,73 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]); | }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]); | ||||
| const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { | 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) { | 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; | return; | ||||
| } | } | ||||
| try { | 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 currentActualPickQty = lot.actualPickQty || 0; | ||||
| const cumulativeQty = currentActualPickQty + submitQty; | 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'; | let newStatus = 'partially_completed'; | ||||
| if (cumulativeQty >= lot.requiredQty) { | if (cumulativeQty >= lot.requiredQty) { | ||||
| @@ -1269,7 +1325,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| await updateStockOutLineStatus({ | await updateStockOutLineStatus({ | ||||
| id: lot.stockOutLineId, | id: lot.stockOutLineId, | ||||
| status: newStatus, | status: newStatus, | ||||
| qty: cumulativeQty // Use cumulative quantity | |||||
| qty: cumulativeQty | |||||
| }); | }); | ||||
| if (submitQty > 0) { | 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) { | 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...`); | ||||
| @@ -1910,13 +1966,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| // Add missing required properties from GetPickOrderLineInfo interface | // Add missing required properties from GetPickOrderLineInfo interface | ||||
| availableQty: selectedLotForExecutionForm.availableQty || 0, | availableQty: selectedLotForExecutionForm.availableQty || 0, | ||||
| requiredQty: selectedLotForExecutionForm.requiredQty || 0, | requiredQty: selectedLotForExecutionForm.requiredQty || 0, | ||||
| uomCode: selectedLotForExecutionForm.uomCode || '', | |||||
| uomDesc: selectedLotForExecutionForm.uomDesc || '', | 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} | pickOrderId={selectedLotForExecutionForm.pickOrderId} | ||||
| pickOrderCreateDate={new Date()} | 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> | </FormProvider> | ||||
| @@ -44,6 +44,9 @@ interface LotPickData { | |||||
| stockOutLineId?: number; | stockOutLineId?: number; | ||||
| stockOutLineStatus?: string; | stockOutLineStatus?: string; | ||||
| stockOutLineQty?: number; | stockOutLineQty?: number; | ||||
| pickOrderLineId?: number; | |||||
| pickOrderId?: number; | |||||
| pickOrderCode?: string; | |||||
| } | } | ||||
| interface PickExecutionFormProps { | interface PickExecutionFormProps { | ||||
| @@ -54,6 +57,7 @@ interface PickExecutionFormProps { | |||||
| selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | ||||
| pickOrderId?: number; | pickOrderId?: number; | ||||
| pickOrderCreateDate: any; | pickOrderCreateDate: any; | ||||
| onNormalPickSubmit?: (lot: LotPickData, submitQty: number) => Promise<void>; | |||||
| // 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>; | // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>; | ||||
| // selectedRowId?: number | null; | // selectedRowId?: number | null; | ||||
| @@ -76,9 +80,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| selectedPickOrderLine, | selectedPickOrderLine, | ||||
| pickOrderId, | pickOrderId, | ||||
| pickOrderCreateDate, | pickOrderCreateDate, | ||||
| // Remove these props | |||||
| // onNormalPickSubmit, | |||||
| // selectedRowId, | |||||
| onNormalPickSubmit, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | 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 [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | ||||
| const [verifiedQty, setVerifiedQty] = useState<number>(0); | const [verifiedQty, setVerifiedQty] = useState<number>(0); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | ||||
| return lot.availableQty || 0; | 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 | // The actualPickQty in the form should be independent of the database value | ||||
| return lot.requiredQty || 0; | return lot.requiredQty || 0; | ||||
| }, []); | }, []); | ||||
| useEffect(() => { | |||||
| console.log('PickExecutionForm props:', { | |||||
| open, | |||||
| onNormalPickSubmit: typeof onNormalPickSubmit, | |||||
| hasOnNormalPickSubmit: !!onNormalPickSubmit, | |||||
| onSubmit: typeof onSubmit, | |||||
| }); | |||||
| }, [open, onNormalPickSubmit, onSubmit]); | |||||
| // 获取处理人员列表 | // 获取处理人员列表 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchHandlers = async () => { | const fetchHandlers = async () => { | ||||
| @@ -184,36 +196,52 @@ useEffect(() => { | |||||
| if (verifiedQty === undefined || verifiedQty < 0) { | if (verifiedQty === undefined || verifiedQty < 0) { | ||||
| newErrors.actualPickQty = t('Qty is required'); | newErrors.actualPickQty = t('Qty is required'); | ||||
| } | } | ||||
| // 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0 | |||||
| // if (verifiedQty > receivedQty) { ... } ← 删除 | |||||
| // 只检查总和是否等于需求数量 | |||||
| const totalQty = verifiedQty + badItemQty + missQty; | 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'); | 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); | setErrors(newErrors); | ||||
| return Object.keys(newErrors).length === 0; | return Object.keys(newErrors).length === 0; | ||||
| }; | }; | ||||
| const handleSubmit = async () => { | 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) { | if (!validateForm() || !formData.pickOrderId) { | ||||
| return; | return; | ||||
| } | } | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| // Use the verified quantity in the submission | |||||
| const submissionData = { | const submissionData = { | ||||
| ...formData, | ...formData, | ||||
| actualPickQty: verifiedQty, | actualPickQty: verifiedQty, | ||||
| @@ -387,7 +387,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| }, | }, | ||||
| [pickOrders, t, tabIndex, items], | [pickOrders, t, tabIndex, items], | ||||
| ); | ); | ||||
| const handleSwitchToRecordTab = useCallback(() => { | |||||
| setTabIndex(1); // 切换到 CompleteJobOrderRecord 标签页(tabIndex 1) | |||||
| }, []); | |||||
| const fetchNewPagePickOrder = useCallback( | const fetchNewPagePickOrder = useCallback( | ||||
| async ( | async ( | ||||
| pagingController: Record<string, number>, | pagingController: Record<string, number>, | ||||
| @@ -438,10 +440,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| <Grid item xs={8}> | <Grid item xs={8}> | ||||
| </Grid> | </Grid> | ||||
| {/* Last 2 buttons aligned right | |||||
| {/* Last 2 buttons aligned right */} | |||||
| <Grid item xs={6} > | <Grid item xs={6} > | ||||
| {/* Unassigned Job Orders */} | |||||
| {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( | {!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && ( | ||||
| <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | <Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}> | ||||
| <Typography variant="h6" gutterBottom> | <Typography variant="h6" gutterBottom> | ||||
| @@ -463,7 +465,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| </Grid> | </Grid> | ||||
| */} | |||||
| </Grid> | </Grid> | ||||
| </Stack> | </Stack> | ||||
| @@ -474,9 +476,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| borderBottom: '1px solid #e0e0e0' | borderBottom: '1px solid #e0e0e0' | ||||
| }}> | }}> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <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("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("Job Order Match")} iconPosition="end" /> */} | ||||
| {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | {/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */} | ||||
| </Tabs> | </Tabs> | ||||
| @@ -487,9 +490,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1; | |||||
| <Box sx={{ | <Box sx={{ | ||||
| p: 2 | p: 2 | ||||
| }}> | }}> | ||||
| {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} | |||||
| {/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */} | |||||
| {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | {tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />} | ||||
| {/* {tabIndex === 2 && <JoPickOrderList />} */} | |||||
| {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />} | |||||
| {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | {/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */} | ||||
| {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | {/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */} | ||||
| </Box> | </Box> | ||||
| @@ -67,6 +67,7 @@ import FGPickOrderCard from "./FGPickOrderCard"; | |||||
| import LotConfirmationModal from "./LotConfirmationModal"; | import LotConfirmationModal from "./LotConfirmationModal"; | ||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| onSwitchToRecordTab: () => void; | |||||
| } | } | ||||
| // QR Code Modal Component (from GoodPickExecution) | // 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 { t } = useTranslation("jo"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -1180,11 +1181,69 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| try { | 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 currentActualPickQty = lot.actualPickQty || 0; | ||||
| const cumulativeQty = currentActualPickQty + submitQty; | 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'; | let newStatus = 'partially_completed'; | ||||
| if (cumulativeQty >= lot.requiredQty) { | if (cumulativeQty >= lot.requiredQty) { | ||||
| @@ -1207,7 +1266,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| await updateStockOutLineStatus({ | await updateStockOutLineStatus({ | ||||
| id: lot.stockOutLineId, | id: lot.stockOutLineId, | ||||
| status: newStatus, | status: newStatus, | ||||
| qty: cumulativeQty // Use cumulative quantity | |||||
| qty: cumulativeQty | |||||
| }); | }); | ||||
| if (submitQty > 0) { | 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) { | 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...`); | ||||
| @@ -1250,7 +1309,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error submitting pick quantity:", error); | console.error("Error submitting pick quantity:", error); | ||||
| } | } | ||||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | |||||
| }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]); | |||||
| const handleSubmitAllScanned = useCallback(async () => { | const handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | const scannedLots = combinedLotData.filter(lot => | ||||
| lot.stockOutLineStatus === 'checked' | lot.stockOutLineStatus === 'checked' | ||||
| @@ -1306,6 +1365,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| checkAndAutoAssignNext(); | checkAndAutoAssignNext(); | ||||
| if (onSwitchToRecordTab) { | |||||
| onSwitchToRecordTab(); | |||||
| } | |||||
| }, 2000); | }, 2000); | ||||
| } else { | } else { | ||||
| console.error("Batch submit failed:", result); | console.error("Batch submit failed:", result); | ||||
| @@ -1318,7 +1380,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } finally { | } finally { | ||||
| setIsSubmittingAll(false); | setIsSubmittingAll(false); | ||||
| } | } | ||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId]) | |||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab]) | |||||
| // Calculate scanned items count | // Calculate scanned items count | ||||
| const scannedItemsCount = useMemo(() => { | const scannedItemsCount = useMemo(() => { | ||||
| @@ -1852,13 +1914,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| // Add missing required properties from GetPickOrderLineInfo interface | // Add missing required properties from GetPickOrderLineInfo interface | ||||
| availableQty: selectedLotForExecutionForm.availableQty || 0, | availableQty: selectedLotForExecutionForm.availableQty || 0, | ||||
| requiredQty: selectedLotForExecutionForm.requiredQty || 0, | requiredQty: selectedLotForExecutionForm.requiredQty || 0, | ||||
| uomCode: selectedLotForExecutionForm.uomCode || '', | |||||
| uomDesc: selectedLotForExecutionForm.uomDesc || '', | 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} | pickOrderId={selectedLotForExecutionForm.pickOrderId} | ||||
| pickOrderCreateDate={new Date()} | 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> | </FormProvider> | ||||
| @@ -14,10 +14,16 @@ import { | |||||
| Tabs, | Tabs, | ||||
| Tab, | Tab, | ||||
| TabsProps, | TabsProps, | ||||
| IconButton, | |||||
| Dialog, | |||||
| DialogTitle, | |||||
| DialogContent, | |||||
| DialogActions, | |||||
| InputAdornment | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import { useTranslation } from "react-i18next"; | 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 ProductionProcessDetail from "./ProductionProcessDetail"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; | 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 { releaseJo, startJo } from "@/app/api/jo/actions"; | ||||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | ||||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | import ProcessSummaryHeader from "./ProcessSummaryHeader"; | ||||
| import EditIcon from "@mui/icons-material/Edit"; | |||||
| interface JobOrderLine { | interface JobOrderLine { | ||||
| id: number; | id: number; | ||||
| jobOrderId: number; | jobOrderId: number; | ||||
| @@ -64,8 +71,9 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null); | ||||
| // 获取数据 | |||||
| const [operationPriority, setOperationPriority] = useState<number>(50); | |||||
| const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); | |||||
| const fetchData = useCallback(async () => { | const fetchData = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| @@ -117,7 +125,25 @@ const getStockAvailable = (line: JobOrderLine) => { | |||||
| } | } | ||||
| return line.stockQty || 0; | 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) => { | const isStockSufficient = (line: JobOrderLine) => { | ||||
| if (line.type?.toLowerCase() === "consumables") { | if (line.type?.toLowerCase() === "consumables") { | ||||
| return false; | return false; | ||||
| @@ -248,12 +274,21 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <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> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| @@ -334,9 +369,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | 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})`; | return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`; | ||||
| }, | }, | ||||
| @@ -350,14 +383,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| type: "number", | type: "number", | ||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | ||||
| // 如果是 consumables,显示 N/A | // 如果是 consumables,显示 N/A | ||||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||||
| return t("N/A"); | |||||
| } | |||||
| const stockAvailable = getStockAvailable(params.row); | 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", | headerAlign: "center", | ||||
| type: "boolean", | type: "boolean", | ||||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | 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) | return isStockSufficient(params.row) | ||||
| ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | ? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | ||||
| : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />; | : <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />; | ||||
| @@ -520,11 +547,36 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| {tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />} | {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> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default ProductionProcessJobOrderDetail; | export default ProductionProcessJobOrderDetail; | ||||
| @@ -195,6 +195,7 @@ | |||||
| "Remark": "明細", | "Remark": "明細", | ||||
| "Req. Qty": "需求數量", | "Req. Qty": "需求數量", | ||||
| "Seq No": "加入步驟", | "Seq No": "加入步驟", | ||||
| "Total pick orders": "總提料單數量", | |||||
| "Seq No Remark": "序號明細", | "Seq No Remark": "序號明細", | ||||
| "Stock Available": "庫存可用", | "Stock Available": "庫存可用", | ||||
| "Confirm": "確認", | "Confirm": "確認", | ||||
| @@ -43,6 +43,7 @@ | |||||
| "Item Code": "成品/半成品編號", | "Item Code": "成品/半成品編號", | ||||
| "Paused": "已暫停", | "Paused": "已暫停", | ||||
| "paused": "已暫停", | "paused": "已暫停", | ||||
| "Total pick orders": "總提料單數量", | |||||
| "Pause Reason": "暫停原因", | "Pause Reason": "暫停原因", | ||||
| "Reason": "原因", | "Reason": "原因", | ||||
| "Stock Available": "倉庫可用數", | "Stock Available": "倉庫可用數", | ||||