| @@ -181,7 +181,17 @@ export const fetchTicketReleaseTable = cache(async ()=> { | |||
| } | |||
| ); | |||
| }); | |||
| export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => { | |||
| const { doId, userId } = data; | |||
| return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | |||
| `${BASE_API_URL}/doPickOrder/batch-release/async-single?userId=${userId}`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(doId), | |||
| headers: { "Content-Type": "application/json" }, | |||
| } | |||
| ); | |||
| }); | |||
| export const startBatchReleaseAsync = cache(async (data: { ids: number[]; userId: number }) => { | |||
| const { ids, userId } = data; | |||
| return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | |||
| @@ -383,6 +383,8 @@ export interface JobOrderProcessLineDetailResponse { | |||
| byproductName: string; | |||
| byproductQty: number; | |||
| byproductUom: string; | |||
| productProcessIssueId: number; | |||
| productProcessIssueStatus: string; | |||
| } | |||
| export interface JobOrderLineInfo { | |||
| id: number, | |||
| @@ -453,6 +455,90 @@ export interface JobTypeResponse { | |||
| id: number; | |||
| name: string; | |||
| } | |||
| export interface SaveProductProcessIssueTimeRequest { | |||
| productProcessLineId: number; | |||
| reason: string; | |||
| } | |||
| export interface JobOrderLotsHierarchicalResponse { | |||
| pickOrder: PickOrderInfoResponse; | |||
| pickOrderLines: PickOrderLineWithLotsResponse[]; | |||
| } | |||
| export interface PickOrderInfoResponse { | |||
| id: number | null; | |||
| code: string | null; | |||
| consoCode: string | null; | |||
| targetDate: string | null; | |||
| type: string | null; | |||
| status: string | null; | |||
| assignTo: number | null; | |||
| jobOrder: JobOrderBasicInfoResponse; | |||
| } | |||
| export interface JobOrderBasicInfoResponse { | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| } | |||
| export interface PickOrderLineWithLotsResponse { | |||
| id: number; | |||
| itemId: number | null; | |||
| itemCode: string | null; | |||
| itemName: string | null; | |||
| requiredQty: number | null; | |||
| uomCode: string | null; | |||
| uomDesc: string | null; | |||
| status: string | null; | |||
| lots: LotDetailResponse[]; | |||
| } | |||
| export interface LotDetailResponse { | |||
| lotId: number | null; | |||
| lotNo: string | null; | |||
| expiryDate: string | null; | |||
| location: string | null; | |||
| availableQty: number | null; | |||
| requiredQty: number | null; | |||
| actualPickQty: number | null; | |||
| processingStatus: string | null; | |||
| lotAvailability: string | null; | |||
| pickOrderId: number | null; | |||
| pickOrderCode: string | null; | |||
| pickOrderConsoCode: string | null; | |||
| pickOrderLineId: number | null; | |||
| stockOutLineId: number | null; | |||
| suggestedPickLotId: number | null; | |||
| stockOutLineQty: number | null; | |||
| stockOutLineStatus: string | null; | |||
| routerIndex: number | null; | |||
| routerArea: string | null; | |||
| routerRoute: string | null; | |||
| uomShortDesc: string | null; | |||
| matchStatus?: string | null; | |||
| matchBy?: number | null; | |||
| matchQty?: number | null; | |||
| } | |||
| export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/issue`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(request), | |||
| } | |||
| ); | |||
| }); | |||
| export const saveProductProcessResumeTime = cache(async (productProcessIssueId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/resume/${productProcessIssueId}`, | |||
| { | |||
| method: "POST", | |||
| } | |||
| ); | |||
| }); | |||
| export const deleteJobOrder=cache(async (jobOrderId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`, | |||
| @@ -480,7 +566,7 @@ export const updateJoPickOrderHandledBy = cache(async (request: UpdateJoPickOrde | |||
| ); | |||
| }); | |||
| export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => { | |||
| return serverFetchJson<any>( | |||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||
| `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`, | |||
| { | |||
| method: "GET", | |||
| @@ -568,7 +654,7 @@ export const startProductProcessLine = async (lineId: number) => { | |||
| }; | |||
| export const completeProductProcessLine = async (lineId: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/product-process/demo/ProcessLine/complete/${lineId}`, | |||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/complete/${lineId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| @@ -715,7 +801,7 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe | |||
| // 获取 Job Order 分层数据 | |||
| export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | |||
| return serverFetchJson<any>( | |||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||
| `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, | |||
| { | |||
| method: "GET", | |||
| @@ -30,6 +30,8 @@ export interface JobOrder { | |||
| type: string; | |||
| jobTypeId: number; | |||
| jobTypeName: string; | |||
| sufficientCount: number; | |||
| insufficientCount: number; | |||
| // TODO pack below into StockInLineInfo | |||
| stockInLineId?: number; | |||
| stockInLineStatus?: string; | |||
| @@ -1063,27 +1063,7 @@ export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId: | |||
| }; | |||
| } | |||
| }; | |||
| // Update the existing function to use the non-auto-assign endpoint | |||
| export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Promise<any[]> => { | |||
| try { | |||
| console.log("🔍 Fetching all pick order line lot details for userId:", userId); | |||
| // Use the non-auto-assign endpoint | |||
| const data = await serverFetchJson<any[]>( | |||
| `${BASE_API_URL}/pickOrder/all-lots-with-details-no-auto-assign/${userId}`, | |||
| { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| } | |||
| ); | |||
| console.log(" Fetched lot details:", data); | |||
| return data; | |||
| } catch (error) { | |||
| console.error("❌ Error fetching lot details:", error); | |||
| return []; | |||
| } | |||
| }); | |||
| export const fetchAllPickOrderDetails = cache(async (userId?: number) => { | |||
| if (!userId) { | |||
| return { | |||
| @@ -25,7 +25,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| //const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const [releaseType, setReleaseType] = useState<string>("batch"); | |||
| const loadSummaries = useCallback(async () => { | |||
| setIsLoadingSummary(true); | |||
| try { | |||
| @@ -40,8 +40,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| } | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchStoreLaneSummary("2/F", dateParam), | |||
| fetchStoreLaneSummary("4/F", dateParam) | |||
| fetchStoreLaneSummary("2/F", dateParam, releaseType), | |||
| fetchStoreLaneSummary("4/F", dateParam, releaseType) | |||
| ]); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| @@ -50,7 +50,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| } finally { | |||
| setIsLoadingSummary(false); | |||
| } | |||
| }, [selectedDate]); | |||
| }, [selectedDate, releaseType]); | |||
| // 初始化 | |||
| useEffect(() => { | |||
| @@ -168,10 +168,34 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| {t("Day After Tomorrow")} ({getDateLabel(2)}) | |||
| </MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| <Box sx={{minWidth: 140, maxWidth: 300 }}> | |||
| <FormControl fullWidth size="small"> | |||
| <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel> | |||
| <Select | |||
| labelId="release-type-select-label" | |||
| id="release-type-select" | |||
| value={releaseType} | |||
| label={t("Release Type")} | |||
| onChange={(e) => { { | |||
| setReleaseType(e.target.value); | |||
| loadSummaries(); | |||
| }}} | |||
| > | |||
| <MenuItem value="batch"> | |||
| {t("Batch")} | |||
| </MenuItem> | |||
| <MenuItem value="single"> | |||
| {t("Single")} | |||
| </MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| p: 1, | |||
| @@ -186,6 +210,13 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| {t("EDT - Lane Code (Unassigned/Total)")} | |||
| </Typography> | |||
| </Box> | |||
| </Stack> | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}> | |||
| </Stack> | |||
| @@ -391,6 +422,11 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -120,20 +120,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| }; | |||
| const getStockCounts = (jo: JobOrder) => { | |||
| const detailedJo = detailedJos.get(jo.id); | |||
| if (!detailedJo?.pickLines || detailedJo.pickLines.length === 0) { | |||
| return { total: 0, sufficient: 0, insufficient: 0 }; | |||
| } | |||
| const totalLines = detailedJo.pickLines.length; | |||
| const sufficientLines = detailedJo.pickLines.filter(pickLine => isStockSufficient(pickLine)).length; | |||
| const insufficientLines = totalLines - sufficientLines; | |||
| return { | |||
| total: totalLines, | |||
| sufficient: sufficientLines, | |||
| insufficient: insufficientLines | |||
| sufficient: jo.sufficientCount, | |||
| insufficient: jo.insufficientCount | |||
| }; | |||
| }; | |||
| @@ -210,7 +200,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| const stockCounts = getStockCounts(row); | |||
| return ( | |||
| <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}> | |||
| {stockCounts.sufficient}/{stockCounts.total} | |||
| {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient} | |||
| </span> | |||
| ); | |||
| } | |||
| @@ -229,17 +219,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| // onClick: (record) => onDetailClick(record), | |||
| // buttonIcon: <EditNote />, | |||
| renderCell: (row) => { | |||
| const btnSx = getButtonSx(row); | |||
| //const btnSx = getButtonSx(row); | |||
| return ( | |||
| <Button | |||
| id="emailSupplier" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ width: "150px", backgroundColor: btnSx.color }} | |||
| // sx={{ width: "150px", backgroundColor: btnSx.color }} | |||
| sx={{ width: "150px" }} | |||
| // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"} | |||
| onClick={() => onDetailClick(row)} | |||
| >{btnSx.label}</Button> | |||
| // >{btnSx.label} | |||
| >{t("View")} | |||
| </Button> | |||
| ) | |||
| } | |||
| }, | |||
| @@ -349,7 +342,7 @@ 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") { | |||
| @@ -367,7 +360,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| 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 | |||
| @@ -34,13 +34,19 @@ import { | |||
| checkPickOrderCompletion, | |||
| PickOrderCompletionResponse, | |||
| checkAndCompletePickOrderByConsoCode, | |||
| confirmLotSubstitution | |||
| confirmLotSubstitution, | |||
| updateStockOutLineStatusByQRCodeAndLotNo, | |||
| batchSubmitList, | |||
| batchSubmitListRequest, | |||
| batchSubmitListLineRequest, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| fetchJobOrderLotsHierarchical, | |||
| fetchUnassignedJobOrderPickOrders, | |||
| assignJobOrderPickOrder | |||
| assignJobOrderPickOrder, | |||
| updateJo, | |||
| JobOrderLotsHierarchicalResponse, | |||
| } from "@/app/api/jo/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { | |||
| @@ -322,14 +328,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null); | |||
| // 修改:使用 Job Order 数据结构 | |||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||
| const [filteredLotData, setFilteredLotData] = useState<any[]>([]); | |||
| // 添加未分配订单状态 | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| @@ -375,6 +380,60 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| const getAllLotsFromHierarchical = useCallback(( | |||
| data: JobOrderLotsHierarchicalResponse | null | |||
| ): any[] => { | |||
| if (!data || !data.pickOrder || !data.pickOrderLines) { | |||
| return []; | |||
| } | |||
| const allLots: any[] = []; | |||
| data.pickOrderLines.forEach((line) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot) => { | |||
| allLots.push({ | |||
| ...lot, | |||
| pickOrderLineId: line.id, | |||
| itemId: line.itemId, | |||
| itemCode: line.itemCode, | |||
| itemName: line.itemName, | |||
| uomCode: line.uomCode, | |||
| uomDesc: line.uomDesc, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| jobOrderId: data.pickOrder.jobOrder.id, | |||
| jobOrderCode: data.pickOrder.jobOrder.code, | |||
| // 添加 pickOrder 信息(如果需要) | |||
| pickOrderId: data.pickOrder.id, | |||
| pickOrderCode: data.pickOrder.code, | |||
| pickOrderConsoCode: data.pickOrder.consoCode, | |||
| pickOrderTargetDate: data.pickOrder.targetDate, | |||
| pickOrderType: data.pickOrder.type, | |||
| pickOrderStatus: data.pickOrder.status, | |||
| pickOrderAssignTo: data.pickOrder.assignTo, | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| return allLots; | |||
| }, []); | |||
| const allLotsFromData = useMemo(() => { | |||
| return getAllLotsFromHierarchical(jobOrderData); | |||
| }, [jobOrderData, getAllLotsFromHierarchical]); | |||
| // 用于显示的 combinedLotData(支持搜索过滤) | |||
| const combinedLotData = useMemo(() => { | |||
| return filteredLotData.length > 0 ? filteredLotData : allLotsFromData; | |||
| }, [filteredLotData, allLotsFromData]); | |||
| // 用于搜索的原始数据 | |||
| const originalCombinedData = useMemo(() => { | |||
| return allLotsFromData; | |||
| }, [allLotsFromData]); | |||
| // 修改:加载未分配的 Job Order 订单 | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| @@ -466,113 +525,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| try { | |||
| const userIdToUse = userId || currentUserId; | |||
| console.log(" fetchJobOrderData called with userId:", userIdToUse); | |||
| if (!userIdToUse) { | |||
| console.warn("⚠️ No userId available, skipping API call"); | |||
| setJobOrderData(null); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| return; | |||
| } | |||
| window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | |||
| detail: { | |||
| hasData: false, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // 使用 Job Order API | |||
| // 直接使用类型化的响应 | |||
| const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); | |||
| console.log(" Job Order data:", jobOrderData); | |||
| console.log(" Job Order data (hierarchical):", jobOrderData); | |||
| setJobOrderData(jobOrderData); | |||
| // Transform hierarchical data to flat structure for the table | |||
| const flatLotData: any[] = []; | |||
| if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { | |||
| jobOrderData.pickOrderLines.forEach((line: any) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot: any) => { | |||
| flatLotData.push({ | |||
| // Pick order info | |||
| pickOrderId: jobOrderData.pickOrder.id, | |||
| pickOrderCode: jobOrderData.pickOrder.code, | |||
| pickOrderConsoCode: jobOrderData.pickOrder.consoCode, | |||
| pickOrderTargetDate: jobOrderData.pickOrder.targetDate, | |||
| pickOrderType: jobOrderData.pickOrder.type, | |||
| pickOrderStatus: jobOrderData.pickOrder.status, | |||
| pickOrderAssignTo: jobOrderData.pickOrder.assignTo, | |||
| // Pick order line info | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| // Item info | |||
| itemId: line.itemId, | |||
| itemCode: line.itemCode, | |||
| itemName: line.itemName, | |||
| uomCode: line.uomCode, | |||
| uomDesc: line.uomDesc, | |||
| // Lot info | |||
| lotId: lot.lotId, | |||
| lotNo: lot.lotNo, | |||
| expiryDate: lot.expiryDate, | |||
| location: lot.location, | |||
| availableQty: lot.availableQty, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: lot.actualPickQty, | |||
| lotStatus: lot.lotStatus, | |||
| lotAvailability: lot.lotAvailability, | |||
| processingStatus: lot.processingStatus, | |||
| stockOutLineId: lot.stockOutLineId, | |||
| stockOutLineStatus: lot.stockOutLineStatus, | |||
| stockOutLineQty: lot.stockOutLineQty, | |||
| suggestedPickLotId: lot.suggestedPickLotId, | |||
| // Router info | |||
| routerIndex: lot.routerIndex, | |||
| secondQrScanStatus: lot.secondQrScanStatus, | |||
| routerArea: lot.routerArea, | |||
| routerRoute: lot.routerRoute, | |||
| uomShortDesc: lot.uomShortDesc | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| // 使用辅助函数获取所有 lots(用于计算完成状态等) | |||
| const allLots = getAllLotsFromHierarchical(jobOrderData); | |||
| setFilteredLotData(allLots); | |||
| const hasData = allLots.length > 0; | |||
| console.log(" Transformed flat lot data:", flatLotData); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| const hasData = flatLotData.length > 0; | |||
| window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | |||
| detail: { | |||
| hasData: hasData, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // 计算完成状态并发送事件 | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => | |||
| // 计算完成状态 | |||
| const allCompleted = allLots.length > 0 && allLots.every((lot) => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| // 发送完成状态事件,包含标签页信息 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 0 // 明确指定这是来自标签页 0 的事件 | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching job order data:", error); | |||
| setJobOrderData(null); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| // 如果加载失败,禁用打印按钮 | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| @@ -582,7 +581,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } finally { | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, [currentUserId]); | |||
| }, [currentUserId, getAllLotsFromHierarchical]); | |||
| // 修改:初始化时加载数据 | |||
| useEffect(() => { | |||
| @@ -828,28 +827,70 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setIsConfirmingLot(false); | |||
| } | |||
| }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); | |||
| const handleFastQrScan = useCallback(async (lotNo: string) => { | |||
| const matchingLot = combinedLotData.find(lot => | |||
| lot.lotNo && lot.lotNo === lotNo | |||
| ); | |||
| if (!matchingLot || !matchingLot.stockOutLineId) { | |||
| console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`); | |||
| return; | |||
| } | |||
| try { | |||
| const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | |||
| pickOrderLineId: matchingLot.pickOrderLineId, | |||
| inventoryLotNo: lotNo, | |||
| stockOutLineId: matchingLot.stockOutLineId, | |||
| itemId: matchingLot.itemId, | |||
| status: "checked", | |||
| }); | |||
| if (res.code === "checked" || res.code === "SUCCESS") { | |||
| const entity = res.entity as any; | |||
| // ✅ 更新 filteredLotData(如果存在)或刷新数据 | |||
| if (filteredLotData.length > 0) { | |||
| setFilteredLotData(prev => prev.map((lot: any) => { | |||
| if (lot.stockOutLineId === matchingLot.stockOutLineId && | |||
| lot.pickOrderLineId === matchingLot.pickOrderLineId) { | |||
| return { | |||
| ...lot, | |||
| stockOutLineStatus: 'checked', | |||
| stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty, | |||
| }; | |||
| } | |||
| return lot; | |||
| })); | |||
| } | |||
| // ✅ 刷新 jobOrderData 以更新所有计算值 | |||
| await fetchJobOrderData(); | |||
| console.log("✅ Fast scan completed successfully"); | |||
| } | |||
| } catch (error) { | |||
| console.error(`❌ Fast scan error for ${lotNo}:`, error); | |||
| } | |||
| }, [combinedLotData, filteredLotData, fetchJobOrderData]); | |||
| const processOutsideQrCode = useCallback(async (latestQr: string) => { | |||
| // Don't process if confirmation modal is open | |||
| // Don't process if confirmation modal is open | |||
| if (lotConfirmationOpen) { | |||
| console.log("⏸️ Confirmation modal is open, skipping QR processing"); | |||
| return; | |||
| } | |||
| // 1) Parse JSON safely | |||
| let qrData: any = null; | |||
| try { | |||
| qrData = JSON.parse(latestQr); | |||
| } catch { | |||
| console.log("QR is not JSON format"); | |||
| // Handle non-JSON QR codes as direct lot numbers | |||
| const directLotNo = latestQr.replace(/[{}]/g, ''); | |||
| if (directLotNo) { | |||
| console.log(`Processing direct lot number: ${directLotNo}`); | |||
| await handleQrCodeSubmit(directLotNo); | |||
| } | |||
| console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| try { | |||
| // Only use the new API when we have JSON with stockInLineId + itemId | |||
| if (!(qrData?.stockInLineId && qrData?.itemId)) { | |||
| @@ -859,18 +900,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return; | |||
| } | |||
| // First, fetch stock in line info to get the lot number | |||
| let stockInLineInfo: any; | |||
| try { | |||
| stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId); | |||
| console.log("Stock in line info:", stockInLineInfo); | |||
| } catch (error) { | |||
| console.error("Error fetching stock in line info:", error); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| // Call new analyze-qr-code API | |||
| const analysis = await analyzeQrCode({ | |||
| itemId: qrData.itemId, | |||
| @@ -892,13 +921,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } = analysis || {}; | |||
| // 1) Find all lots for the same item from current expected list | |||
| const sameItemLotsInExpected = combinedLotData.filter(l => | |||
| const sameItemLotsInExpected = combinedLotData.filter((l: any) => | |||
| (l.itemId && analyzedItemId && l.itemId === analyzedItemId) || | |||
| (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) | |||
| ); | |||
| if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { | |||
| // Case 3: No item code match | |||
| console.error("No item match in expected lots for scanned code"); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| @@ -906,7 +934,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| // Find the ACTIVE suggested lot (not rejected lots) | |||
| const activeSuggestedLots = sameItemLotsInExpected.filter(lot => | |||
| const activeSuggestedLots = sameItemLotsInExpected.filter((lot: any) => | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'completed' | |||
| @@ -919,21 +947,78 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| return; | |||
| } | |||
| // Use the first active suggested lot as the "expected" lot | |||
| // 2) Check if scanned lot is exactly in active suggested lots | |||
| const exactLotMatch = activeSuggestedLots.find((l: any) => | |||
| (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || | |||
| (scanned?.lotNo && l.lotNo === scanned.lotNo) | |||
| ); | |||
| if (exactLotMatch && scanned?.lotNo) { | |||
| // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) | |||
| console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | |||
| if (!exactLotMatch.stockOutLineId) { | |||
| console.warn("No stockOutLineId on exactLotMatch, cannot update status by QR."); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| try { | |||
| const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | |||
| pickOrderLineId: exactLotMatch.pickOrderLineId, | |||
| inventoryLotNo: scanned.lotNo, | |||
| stockOutLineId: exactLotMatch.stockOutLineId, | |||
| itemId: exactLotMatch.itemId, | |||
| status: "checked", | |||
| }); | |||
| if (res.code === "checked" || res.code === "SUCCESS") { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| // ✅ 刷新数据而不是直接更新 state | |||
| await fetchJobOrderData(); | |||
| console.log("✅ Status updated, data refreshed"); | |||
| } else if (res.code === "LOT_NUMBER_MISMATCH") { | |||
| console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else if (res.code === "ITEM_MISMATCH") { | |||
| console.warn("Backend reported ITEM_MISMATCH:", res.message); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else { | |||
| console.warn("Unexpected response code from backend:", res.code); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } | |||
| } catch (e) { | |||
| console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } | |||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||
| } | |||
| // Case 2: Item matches but lot number differs -> open confirmation modal | |||
| const expectedLot = activeSuggestedLots[0]; | |||
| if (!expectedLot) { | |||
| console.error("Could not determine expected lot for confirmation"); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| // 2) Check if the scanned lot matches exactly | |||
| if (scanned?.lotNo === expectedLot.lotNo) { | |||
| // Case 1: Exact match - process normally | |||
| console.log(` Exact lot match: ${scanned.lotNo}`); | |||
| // Check if the expected lot is already the scanned lot (after substitution) | |||
| if (expectedLot.lotNo === scanned?.lotNo) { | |||
| console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); | |||
| await handleQrCodeSubmit(scanned.lotNo); | |||
| return; | |||
| } | |||
| // Case 2: Same item, different lot - show confirmation modal | |||
| console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||
| // DON'T stop scanning - just pause QR processing by showing modal | |||
| setSelectedLotForQr(expectedLot); | |||
| handleLotMismatch( | |||
| { | |||
| @@ -955,7 +1040,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, fetchJobOrderData]); | |||
| const handleManualInputSubmit = useCallback(() => { | |||
| @@ -1229,7 +1314,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | |||
| const handleSubmitAllScanned = useCallback(async () => { | |||
| const scannedLots = combinedLotData.filter(lot => | |||
| lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted | |||
| lot.stockOutLineStatus === 'checked' | |||
| ); | |||
| if (scannedLots.length === 0) { | |||
| @@ -1238,62 +1323,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| setIsSubmittingAll(true); | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`); | |||
| try { | |||
| // Submit all items in parallel using Promise.all | |||
| const submitPromises = scannedLots.map(async (lot) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||
| // 转换为 batchSubmitList 所需的格式 | |||
| const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0; | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + submitQty; | |||
| let newStatus = 'partially_completed'; | |||
| if (cumulativeQty >= lot.requiredQty) { | |||
| if (cumulativeQty >= (lot.requiredQty || 0)) { | |||
| newStatus = 'completed'; | |||
| } | |||
| console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`); | |||
| // Update stock out line | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: newStatus, | |||
| qty: cumulativeQty | |||
| }); | |||
| // Update inventory | |||
| if (submitQty > 0) { | |||
| await updateInventoryLotLineQuantities({ | |||
| inventoryLotLineId: lot.lotId, | |||
| qty: submitQty, | |||
| status: 'available', | |||
| operation: 'pick' | |||
| }); | |||
| } | |||
| // Check if pick order is completed | |||
| if (newStatus === 'completed' && lot.pickOrderConsoCode) { | |||
| await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode); | |||
| } | |||
| return { success: true, lotNo: lot.lotNo }; | |||
| return { | |||
| stockOutLineId: Number(lot.stockOutLineId) || 0, | |||
| pickOrderLineId: Number(lot.pickOrderLineId), | |||
| inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, | |||
| requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0), | |||
| actualPickQty: Number(cumulativeQty), | |||
| stockOutLineStatus: newStatus, | |||
| pickOrderConsoCode: String(lot.pickOrderConsoCode || ''), | |||
| noLot: Boolean(false) // Job Order 通常都有 lot | |||
| }; | |||
| }); | |||
| // Wait for all submissions to complete | |||
| const results = await Promise.all(submitPromises); | |||
| const successCount = results.filter(r => r.success).length; | |||
| const request: batchSubmitListRequest = { | |||
| userId: currentUserId || 0, | |||
| lines: lines | |||
| }; | |||
| console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||
| // 使用 batchSubmitList API | |||
| const result = await batchSubmitList(request); | |||
| console.log(`📥 Batch submit result:`, result); | |||
| // Refresh data once after all submissions | |||
| await fetchJobOrderData(); | |||
| // 刷新数据 | |||
| await fetchJobOrderData(); // 或 pickOrderId,根据页面 | |||
| if (successCount > 0) { | |||
| if (result && result.code === "SUCCESS") { | |||
| setQrScanSuccess(true); | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| checkAndAutoAssignNext(); | |||
| }, 2000); | |||
| } else { | |||
| console.error("Batch submit failed:", result); | |||
| setQrScanError(true); | |||
| } | |||
| } catch (error) { | |||
| @@ -1302,7 +1378,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } finally { | |||
| setIsSubmittingAll(false); | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]); | |||
| // Calculate scanned items count | |||
| const scannedItemsCount = useMemo(() => { | |||
| @@ -1409,38 +1485,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, | |||
| ]; | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| if (!originalCombinedData) return; | |||
| const filtered = originalCombinedData.filter((lot: any) => { | |||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||
| lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||
| const itemCodeMatch = !query.itemCode || | |||
| lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); | |||
| const itemNameMatch = !query.itemName || | |||
| lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); | |||
| const lotNoMatch = !query.lotNo || | |||
| lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase()); | |||
| return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch; | |||
| }); | |||
| setCombinedLotData(filtered); | |||
| console.log("Filtered lots count:", filtered.length); | |||
| }, [originalCombinedData]); | |||
| const handleReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| if (originalCombinedData) { | |||
| setCombinedLotData(originalCombinedData); | |||
| } | |||
| }, [originalCombinedData]); | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| @@ -34,7 +34,11 @@ import { | |||
| checkPickOrderCompletion, | |||
| PickOrderCompletionResponse, | |||
| checkAndCompletePickOrderByConsoCode, | |||
| confirmLotSubstitution | |||
| confirmLotSubstitution, | |||
| updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加 | |||
| batchSubmitList, // ✅ 添加 | |||
| batchSubmitListRequest, // ✅ 添加 | |||
| batchSubmitListLineRequest, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| // 修改:使用 Job Order API | |||
| import { | |||
| @@ -42,7 +46,8 @@ import { | |||
| //fetchUnassignedJobOrderPickOrders, | |||
| assignJobOrderPickOrder, | |||
| fetchJobOrderLotsHierarchicalByPickOrderId, | |||
| updateJoPickOrderHandledBy | |||
| updateJoPickOrderHandledBy, | |||
| JobOrderLotsHierarchicalResponse, | |||
| } from "@/app/api/jo/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { | |||
| @@ -326,11 +331,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // 修改:使用 Job Order 数据结构 | |||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||
| // 添加未分配订单状态 | |||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | |||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | |||
| @@ -343,7 +346,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const [qrScanInput, setQrScanInput] = useState<string>(''); | |||
| const [qrScanError, setQrScanError] = useState<boolean>(false); | |||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | |||
| const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null); | |||
| const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| @@ -376,7 +379,52 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| const getAllLotsFromHierarchical = useCallback(( | |||
| data: JobOrderLotsHierarchicalResponse | null | |||
| ): any[] => { | |||
| if (!data || !data.pickOrder || !data.pickOrderLines) { | |||
| return []; | |||
| } | |||
| const allLots: any[] = []; | |||
| data.pickOrderLines.forEach((line) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot) => { | |||
| allLots.push({ | |||
| ...lot, | |||
| pickOrderLineId: line.id, | |||
| itemId: line.itemId, | |||
| itemCode: line.itemCode, | |||
| itemName: line.itemName, | |||
| uomCode: line.uomCode, | |||
| uomDesc: line.uomDesc, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| jobOrderId: data.pickOrder.jobOrder.id, | |||
| jobOrderCode: data.pickOrder.jobOrder.code, | |||
| // 添加 pickOrder 信息(如果需要) | |||
| pickOrderId: data.pickOrder.id, | |||
| pickOrderCode: data.pickOrder.code, | |||
| pickOrderConsoCode: data.pickOrder.consoCode, | |||
| pickOrderTargetDate: data.pickOrder.targetDate, | |||
| pickOrderType: data.pickOrder.type, | |||
| pickOrderStatus: data.pickOrder.status, | |||
| pickOrderAssignTo: data.pickOrder.assignTo, | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| return allLots; | |||
| }, []); | |||
| const combinedLotData = useMemo(() => { | |||
| return getAllLotsFromHierarchical(jobOrderData); | |||
| }, [jobOrderData, getAllLotsFromHierarchical]); | |||
| const originalCombinedData = useMemo(() => { | |||
| return getAllLotsFromHierarchical(jobOrderData); | |||
| }, [jobOrderData, getAllLotsFromHierarchical]); | |||
| // 修改:加载未分配的 Job Order 订单 | |||
| const loadUnassignedOrders = useCallback(async () => { | |||
| setIsLoadingUnassigned(true); | |||
| @@ -467,120 +515,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| if (!pickOrderId) { | |||
| console.warn("⚠️ No pickOrderId provided, skipping API call"); | |||
| setJobOrderData(null); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| return; | |||
| } | |||
| console.log("🔍 Fetching job order data by pickOrderId:", pickOrderId); | |||
| window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | |||
| detail: { | |||
| hasData: false, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // 直接使用类型化的响应 | |||
| const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId); | |||
| console.log("✅ Job Order data:", jobOrderData); | |||
| console.log("✅ Job Order data (hierarchical):", jobOrderData); | |||
| setJobOrderData(jobOrderData); | |||
| // Transform hierarchical data to flat structure for the table | |||
| const flatLotData: any[] = []; | |||
| if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { | |||
| jobOrderData.pickOrderLines.forEach((line: any) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot: any) => { | |||
| flatLotData.push({ | |||
| pickOrderId: jobOrderData.pickOrder.id, | |||
| pickOrderCode: jobOrderData.pickOrder.code, | |||
| pickOrderConsoCode: jobOrderData.pickOrder.consoCode, | |||
| pickOrderTargetDate: jobOrderData.pickOrder.targetDate, | |||
| pickOrderType: jobOrderData.pickOrder.type, | |||
| pickOrderStatus: jobOrderData.pickOrder.status, | |||
| pickOrderAssignTo: jobOrderData.pickOrder.assignTo, | |||
| // Pick order line info | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| // Item info | |||
| itemId: line.itemId, | |||
| itemCode: line.itemCode, | |||
| itemName: line.itemName, | |||
| uomCode: line.uomCode, | |||
| uomDesc: line.uomDesc, | |||
| // Lot info | |||
| lotId: lot.lotId, | |||
| lotNo: lot.lotNo, | |||
| expiryDate: lot.expiryDate, | |||
| location: lot.location, | |||
| availableQty: lot.availableQty, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: lot.actualPickQty, | |||
| lotStatus: lot.lotStatus, | |||
| lotAvailability: lot.lotAvailability, | |||
| processingStatus: lot.processingStatus, | |||
| stockOutLineId: lot.stockOutLineId, | |||
| stockOutLineStatus: lot.stockOutLineStatus, | |||
| stockOutLineQty: lot.stockOutLineQty, | |||
| suggestedPickLotId: lot.suggestedPickLotId, | |||
| // Router info | |||
| routerIndex: lot.routerIndex, | |||
| secondQrScanStatus: lot.secondQrScanStatus, | |||
| routerArea: lot.routerArea, | |||
| routerRoute: lot.routerRoute, | |||
| uomShortDesc: lot.uomShortDesc | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| console.log("✅ Transformed flat lot data:", flatLotData); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| const hasData = flatLotData.length > 0; | |||
| window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | |||
| detail: { | |||
| hasData: hasData, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // 使用辅助函数获取所有 lots(不再扁平化) | |||
| const allLots = getAllLotsFromHierarchical(jobOrderData); | |||
| // Calculate completion status and send event | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => | |||
| lot.processingStatus === 'completed' | |||
| ); | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: allCompleted, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| // ... 其他逻辑保持不变 ... | |||
| } catch (error) { | |||
| console.error("❌ Error fetching job order data:", error); | |||
| setJobOrderData(null); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { | |||
| allLotsCompleted: false, | |||
| tabIndex: 0 | |||
| } | |||
| })); | |||
| } finally { | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, []); | |||
| }, [getAllLotsFromHierarchical]); | |||
| const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | |||
| if (!currentUserId || !pickOrderId || !itemId) { | |||
| return; | |||
| @@ -796,7 +751,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | |||
| stockOutLineId: selectedLotForQr.stockOutLineId, | |||
| originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, | |||
| newInventoryLotLineId: newLotLineId | |||
| newInventoryLotNo: scannedLotData.lotNo | |||
| }); | |||
| console.log(" Lot substitution result:", substitutionResult); | |||
| @@ -947,10 +902,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // 2) Check if the scanned lot matches exactly | |||
| if (scanned?.lotNo === expectedLot.lotNo) { | |||
| // Case 1: Exact match - process normally | |||
| console.log(` Exact lot match: ${scanned.lotNo}`); | |||
| await handleQrCodeSubmit(scanned.lotNo); | |||
| return; | |||
| // ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快) | |||
| console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`); | |||
| if (!expectedLot.stockOutLineId) { | |||
| console.warn("No stockOutLineId on expectedLot, cannot update status by QR."); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| try { | |||
| const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | |||
| pickOrderLineId: expectedLot.pickOrderLineId, | |||
| inventoryLotNo: scanned.lotNo, | |||
| stockOutLineId: expectedLot.stockOutLineId, | |||
| itemId: expectedLot.itemId, | |||
| status: "checked", | |||
| }); | |||
| if (res.code === "checked" || res.code === "SUCCESS") { | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| // ✅ 刷新数据而不是直接更新 state | |||
| const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||
| await fetchJobOrderData(pickOrderId); | |||
| console.log("✅ Status updated, data refreshed"); | |||
| } else if (res.code === "LOT_NUMBER_MISMATCH") { | |||
| console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else if (res.code === "ITEM_MISMATCH") { | |||
| console.warn("Backend reported ITEM_MISMATCH:", res.message); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } else { | |||
| console.warn("Unexpected response code from backend:", res.code); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } | |||
| } catch (e) { | |||
| console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| } | |||
| return; // ✅ 直接返回,不再调用 handleQrCodeSubmit | |||
| } | |||
| // Case 2: Same item, different lot - show confirmation modal | |||
| @@ -1255,7 +1253,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | |||
| const handleSubmitAllScanned = useCallback(async () => { | |||
| const scannedLots = combinedLotData.filter(lot => | |||
| lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted | |||
| lot.stockOutLineStatus === 'checked' | |||
| ); | |||
| if (scannedLots.length === 0) { | |||
| @@ -1264,94 +1262,54 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } | |||
| setIsSubmittingAll(true); | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||
| console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`); | |||
| try { | |||
| // Submit all items in parallel using Promise.all | |||
| const submitPromises = scannedLots.map(async (lot) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty; | |||
| // ✅ 转换为 batchSubmitList 所需的格式 | |||
| const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => { | |||
| const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0; | |||
| const currentActualPickQty = lot.actualPickQty || 0; | |||
| const cumulativeQty = currentActualPickQty + submitQty; | |||
| let newStatus = 'partially_completed'; | |||
| if (cumulativeQty >= lot.requiredQty) { | |||
| if (cumulativeQty >= (lot.requiredQty || 0)) { | |||
| newStatus = 'completed'; | |||
| } | |||
| console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`); | |||
| // Update stock out line | |||
| await updateStockOutLineStatus({ | |||
| id: lot.stockOutLineId, | |||
| status: newStatus, | |||
| qty: cumulativeQty | |||
| }); | |||
| // Update inventory | |||
| if (submitQty > 0) { | |||
| await updateInventoryLotLineQuantities({ | |||
| inventoryLotLineId: lot.lotId, | |||
| qty: submitQty, | |||
| status: 'available', | |||
| operation: 'pick' | |||
| }); | |||
| } | |||
| // REMOVED: Don't check completion here - do it after all submissions | |||
| // Return the lot info for completion check | |||
| return { | |||
| success: true, | |||
| lotNo: lot.lotNo, | |||
| pickOrderConsoCode: lot.pickOrderConsoCode, | |||
| newStatus: newStatus | |||
| return { | |||
| stockOutLineId: Number(lot.stockOutLineId) || 0, | |||
| pickOrderLineId: Number(lot.pickOrderLineId), | |||
| inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null, | |||
| requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0), | |||
| actualPickQty: Number(cumulativeQty), | |||
| stockOutLineStatus: newStatus, | |||
| pickOrderConsoCode: String(lot.pickOrderConsoCode || ''), | |||
| noLot: Boolean(false) // Job Order 通常都有 lot | |||
| }; | |||
| }); | |||
| // Wait for all submissions to complete | |||
| const results = await Promise.all(submitPromises); | |||
| const successCount = results.filter(r => r.success).length; | |||
| console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`); | |||
| const request: batchSubmitListRequest = { | |||
| userId: currentUserId || 0, | |||
| lines: lines | |||
| }; | |||
| // FIXED: Check completion AFTER all submissions are done | |||
| // Collect unique consoCodes from completed lots | |||
| const completedConsoCodes = new Set<string>(); | |||
| results.forEach(result => { | |||
| if (result.success && result.newStatus === 'completed' && result.pickOrderConsoCode) { | |||
| completedConsoCodes.add(result.pickOrderConsoCode); | |||
| } | |||
| }); | |||
| // ✅ 使用 batchSubmitList API | |||
| const result = await batchSubmitList(request); | |||
| console.log(`📥 Batch submit result:`, result); | |||
| // Check completion for each unique consoCode | |||
| await Promise.all( | |||
| Array.from(completedConsoCodes).map(async (consoCode) => { | |||
| try { | |||
| console.log(`🔍 Checking completion for pick order: ${consoCode}`); | |||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(consoCode); | |||
| console.log(` Pick order completion check result for ${consoCode}:`, completionResponse); | |||
| if (completionResponse.code === "SUCCESS") { | |||
| console.log(`✅ Pick order ${consoCode} completed successfully!`); | |||
| } else if (completionResponse.message === "not completed") { | |||
| console.log(`⏳ Pick order ${consoCode} not completed yet, more lines remaining`); | |||
| } else { | |||
| console.error(`❌ Error checking completion for ${consoCode}: ${completionResponse.message}`); | |||
| } | |||
| } catch (error) { | |||
| console.error(`❌ Error checking pick order completion for ${consoCode}:`, error); | |||
| } | |||
| })); | |||
| // Refresh data once after all submissions and completion checks | |||
| // 刷新数据 | |||
| const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | |||
| await fetchJobOrderData(pickOrderId); | |||
| if (successCount > 0) { | |||
| if (result && result.code === "SUCCESS") { | |||
| setQrScanSuccess(true); | |||
| setTimeout(() => { | |||
| setQrScanSuccess(false); | |||
| checkAndAutoAssignNext(); | |||
| }, 2000); | |||
| } else { | |||
| console.error("Batch submit failed:", result); | |||
| setQrScanError(true); | |||
| } | |||
| } catch (error) { | |||
| @@ -1360,7 +1318,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| } finally { | |||
| setIsSubmittingAll(false); | |||
| } | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId]) | |||
| // Calculate scanned items count | |||
| const scannedItemsCount = useMemo(() => { | |||
| @@ -1469,38 +1427,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| }, | |||
| ]; | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| if (!originalCombinedData) return; | |||
| const filtered = originalCombinedData.filter((lot: any) => { | |||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||
| lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||
| const itemCodeMatch = !query.itemCode || | |||
| lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); | |||
| const itemNameMatch = !query.itemName || | |||
| lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); | |||
| const lotNoMatch = !query.lotNo || | |||
| lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase()); | |||
| return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch; | |||
| }); | |||
| setCombinedLotData(filtered); | |||
| console.log("Filtered lots count:", filtered.length); | |||
| }, [originalCombinedData]); | |||
| const handleReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| if (originalCombinedData) { | |||
| setCombinedLotData(originalCombinedData); | |||
| } | |||
| }, [originalCombinedData]); | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| @@ -620,6 +620,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| const isCompleted = statusLower === 'completed'; | |||
| const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; | |||
| const isPaused = statusLower === 'paused'; | |||
| const isPending = statusLower === 'pending' || status === ''; | |||
| return ( | |||
| @@ -657,6 +658,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| }} /> | |||
| ) : isPending ? ( | |||
| <Chip label={t("Pending")} color="default" size="small" /> | |||
| ) : isPaused ? ( | |||
| <Chip label={t("Paused")} color="warning" size="small" /> | |||
| ) : ( | |||
| <Chip label={t("Unknown")} color="error" size="small" /> | |||
| )} | |||
| @@ -672,7 +675,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||
| > | |||
| {t("Start")} | |||
| </Button> | |||
| ) : statusLower === 'in_progress' || statusLower === 'in progress' ? ( | |||
| ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| @@ -131,7 +131,7 @@ const isStockSufficient = (line: JobOrderLine) => { | |||
| const stockCounts = useMemo(() => { | |||
| // 过滤掉 consumables 类型的 lines | |||
| const nonConsumablesLines = jobOrderLines.filter( | |||
| line => line.type?.toLowerCase() !== "consumables" | |||
| line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" | |||
| ); | |||
| const total = nonConsumablesLines.length; | |||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | |||
| @@ -334,7 +334,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| if (params.row.type?.toLowerCase() === "consumables") { | |||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||
| return t("N/A"); | |||
| } | |||
| @@ -350,7 +350,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| type: "number", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| // 如果是 consumables,显示 N/A | |||
| if (params.row.type?.toLowerCase() === "consumables") { | |||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||
| return t("N/A"); | |||
| } | |||
| const stockAvailable = getStockAvailable(params.row); | |||
| @@ -386,7 +386,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| headerAlign: "center", | |||
| type: "boolean", | |||
| renderCell: (params: GridRenderCellParams<JobOrderLine>) => { | |||
| if (params.row.type?.toLowerCase() === "consumables") { | |||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||
| return <Typography>{t("N/A")}</Typography>; | |||
| } | |||
| return isStockSufficient(params.row) | |||
| @@ -22,6 +22,9 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { | |||
| fetchAllJoborderProductProcessInfo, | |||
| AllJoborderProductProcessInfoResponse, | |||
| updateJo, | |||
| fetchProductProcessesByJobOrderId, | |||
| completeProductProcessLine | |||
| } from "@/app/api/jo/actions"; | |||
| import { StockInLineInput } from "@/app/api/stockIn"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| @@ -55,6 +58,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| }); | |||
| setOpenModal(true); | |||
| }, [t]); | |||
| const fetchProcesses = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| @@ -72,12 +76,48 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| useEffect(() => { | |||
| fetchProcesses(); | |||
| }, [fetchProcesses]); | |||
| const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => { | |||
| if (!process.jobOrderId) { | |||
| alert(t("Invalid Job Order Id")); | |||
| return; | |||
| } | |||
| try { | |||
| setLoading(true); // 可选:已有 loading state 可复用 | |||
| // 1) 拉取该 JO 的所有 process,取出全部 lineId | |||
| const processes = await fetchProductProcessesByJobOrderId(process.jobOrderId); | |||
| const lineIds = (processes ?? []) | |||
| .flatMap(p => (p as any).productProcessLines ?? []) | |||
| .map(l => l.id) | |||
| .filter(Boolean); | |||
| // 2) 逐个调用 completeProductProcessLine | |||
| for (const lineId of lineIds) { | |||
| try { | |||
| await completeProductProcessLine(lineId); | |||
| } catch (e) { | |||
| console.error("completeProductProcessLine failed for lineId:", lineId, e); | |||
| } | |||
| } | |||
| // 3) 更新 JO 状态 | |||
| await updateJo({ id: process.jobOrderId, status: "completed" }); | |||
| // 4) 刷新列表 | |||
| await fetchProcesses(); | |||
| } catch (e) { | |||
| console.error(e); | |||
| alert(t("An error has occurred. Please try again later.")); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [t, fetchProcesses]); | |||
| 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 | |||
| }, []); | |||
| const startIdx = page * PER_PAGE; | |||
| const paged = processes.slice(startIdx, startIdx + PER_PAGE); | |||
| @@ -104,10 +144,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| ? "primary" | |||
| : "default"; | |||
| const finishedCount = | |||
| (process as any).finishedProductProcessLineCount ?? | |||
| (process as any).FinishedProductProcessLineCount ?? | |||
| 0; | |||
| const finishedCount = | |||
| (process.lines || []).filter( | |||
| (l) => String(l.status ?? "").trim().toLowerCase() === "completed" | |||
| ).length; | |||
| const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0; | |||
| const linesWithStatus = (process.lines || []).filter( | |||
| @@ -184,6 +224,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||
| <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}> | |||
| {t("View Details")} | |||
| </Button> | |||
| {statusLower !== "completed" && ( | |||
| <Button variant="contained" size="small" onClick={() => handleUpdateJo(process)}> | |||
| {t("Update Job Order")} | |||
| </Button> | |||
| )} | |||
| {statusLower === "completed" && ( | |||
| <Button onClick={() => handleViewStockIn(process)}> | |||
| {t("view stockin")} | |||
| @@ -11,6 +11,10 @@ import { | |||
| TableCell, | |||
| TableHead, | |||
| TableRow, | |||
| Dialog, | |||
| DialogTitle, | |||
| DialogContent, | |||
| DialogActions, | |||
| Card, | |||
| CardContent, | |||
| Grid, | |||
| @@ -21,7 +25,7 @@ import StopIcon from "@mui/icons-material/Stop"; | |||
| import PauseIcon from "@mui/icons-material/Pause"; | |||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions"; | |||
| import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest,saveProductProcessResumeTime,saveProductProcessIssueTime} from "@/app/api/jo/actions"; | |||
| import { Operator, Machine } from "@/app/api/jo"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| @@ -36,7 +40,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| lineId, | |||
| onBack, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { t } = useTranslation( ["common","jo"]); | |||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | |||
| const isCompleted = lineDetail?.status === "Completed"; | |||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | |||
| @@ -71,6 +75,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | |||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | |||
| const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | |||
| const [pauseReason, setPauseReason] = useState(""); | |||
| // 检查是否两个都已扫描 | |||
| //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | |||
| @@ -126,6 +132,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| const timer = setInterval(update, 1000); | |||
| return () => clearInterval(timer); | |||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); | |||
| const handleSubmitOutput = async () => { | |||
| if (!lineDetail?.id) return; | |||
| @@ -207,22 +214,41 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| // 开始扫描 | |||
| const handlePause = () => { | |||
| setIsPaused(true); | |||
| const handleOpenReasonModel = () => { | |||
| setIsOpenReasonModel(true); | |||
| setPauseReason(""); // 重置原因 | |||
| }; | |||
| const handleContinue = () => { | |||
| setIsPaused(false); | |||
| const handleCloseReasonModel = () => { | |||
| setIsOpenReasonModel(false); | |||
| setPauseReason(""); // 清空原因 | |||
| }; | |||
| const handleStop = () => { | |||
| setIsPaused(false); | |||
| // TODO: 调用停止流程的 API | |||
| const handleSaveReason = async () => { | |||
| if (!pauseReason.trim()) { | |||
| alert(t("Please enter a reason for pausing")); | |||
| return; | |||
| } | |||
| if (!lineDetail?.id) return; | |||
| try { | |||
| await saveProductProcessIssueTime({ | |||
| productProcessLineId: lineDetail.id, | |||
| reason: pauseReason.trim() | |||
| }); | |||
| setIsOpenReasonModel(false); | |||
| setPauseReason(""); | |||
| // 刷新 line detail | |||
| fetchProductProcessLineDetail(lineDetail.id) | |||
| .then((detail) => { | |||
| setLineDetail(detail as any); | |||
| }) | |||
| .catch(err => { | |||
| console.error("Failed to load line detail", err); | |||
| }); | |||
| } catch (error) { | |||
| console.error("Error saving pause reason:", error); | |||
| alert(t("Failed to pause. Please try again.")); | |||
| } | |||
| }; | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ mb: 2 }}> | |||
| @@ -407,16 +433,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| variant="contained" | |||
| color="error" | |||
| startIcon={<StopIcon />} | |||
| onClick={handleStop} | |||
| onClick={() => saveProductProcessIssueTime(lineDetail?.id || 0 as number)} | |||
| > | |||
| {t("Stop")} | |||
| </Button> | |||
| {!isPaused ? ( | |||
| */ | |||
| } | |||
| { lineDetail?.status === 'InProgress'? ( | |||
| <Button | |||
| variant="contained" | |||
| color="warning" | |||
| startIcon={<PauseIcon />} | |||
| onClick={handlePause} | |||
| onClick={() => handleOpenReasonModel()} | |||
| > | |||
| {t("Pause")} | |||
| </Button> | |||
| @@ -425,12 +453,12 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| variant="contained" | |||
| color="success" | |||
| startIcon={<PlayArrowIcon />} | |||
| onClick={handleContinue} | |||
| onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)} | |||
| > | |||
| {t("Continue")} | |||
| </Button> | |||
| )} | |||
| */} | |||
| <Button | |||
| sx={{ mt: 2, alignSelf: "flex-end" }} | |||
| variant="outlined" | |||
| @@ -699,6 +727,39 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||
| )} | |||
| </> | |||
| )} | |||
| <Dialog | |||
| open={isOpenReasonModel} | |||
| onClose={handleCloseReasonModel} | |||
| maxWidth="sm" | |||
| fullWidth | |||
| > | |||
| <DialogTitle>{t("Pause Reason")}</DialogTitle> | |||
| <DialogContent> | |||
| <TextField | |||
| autoFocus | |||
| margin="dense" | |||
| label={t("Reason")} | |||
| fullWidth | |||
| multiline | |||
| rows={4} | |||
| value={pauseReason} | |||
| onChange={(e) => setPauseReason(e.target.value)} | |||
| //required | |||
| /> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={handleCloseReasonModel}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| onClick={handleSaveReason} | |||
| variant="contained" | |||
| disabled={!pauseReason.trim()} | |||
| > | |||
| {t("Confirm")} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </Box> | |||
| ); | |||
| }; | |||
| @@ -13,6 +13,8 @@ | |||
| "code": "編號", | |||
| "Name": "名稱", | |||
| "Type": "類型", | |||
| "Update Job Order": "更新工單", | |||
| "No": "沒有", | |||
| "WIP": "半成品", | |||
| "R&D": "研發", | |||
| @@ -195,10 +197,16 @@ | |||
| "Seq No": "加入步驟", | |||
| "Seq No Remark": "序號明細", | |||
| "Stock Available": "庫存可用", | |||
| "Confirm": "確認", | |||
| "Stock Status": "庫存狀態", | |||
| "Target Production Date": "目標生產日期", | |||
| "id": "ID", | |||
| "Finished lines": "完成行", | |||
| "Finished lines": "已完成流程", | |||
| "Please scan staff no": "請掃描員工編號", | |||
| "Paused": "已暫停", | |||
| "paused": "已暫停", | |||
| "Pause Reason": "暫停原因", | |||
| "Reason": "原因", | |||
| "Invalid Stock In Line Id": "無效庫存行ID", | |||
| "Production date": "生產日期", | |||
| "Required Qty": "需求數量", | |||
| @@ -10,6 +10,7 @@ | |||
| "Picked Qty": "已提料數量", | |||
| "Req. Qty": "需求數量", | |||
| "UoM": "銷售單位", | |||
| "No": "沒有", | |||
| "Status": "工單狀態", | |||
| "Lot No.": "批號", | |||
| "Delete Job Order": "刪除工單", | |||
| @@ -40,7 +41,13 @@ | |||
| "Production Priority": "生產優先度", | |||
| "Sequence": "序", | |||
| "Item Code": "成品/半成品編號", | |||
| "Paused": "已暫停", | |||
| "paused": "已暫停", | |||
| "Pause Reason": "暫停原因", | |||
| "Reason": "原因", | |||
| "Stock Available": "倉庫可用數", | |||
| "Staff No": "員工編號", | |||
| "Please scan staff no": "請掃描員工編號", | |||
| "Stock Status": "可提料", | |||
| "Total lines: ": "所需貨品項目數量: ", | |||
| "Lines with sufficient stock: ": "可提料項目數量: ", | |||