| @@ -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 }) => { | export const startBatchReleaseAsync = cache(async (data: { ids: number[]; userId: number }) => { | ||||
| const { ids, userId } = data; | const { ids, userId } = data; | ||||
| return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | ||||
| @@ -383,6 +383,8 @@ export interface JobOrderProcessLineDetailResponse { | |||||
| byproductName: string; | byproductName: string; | ||||
| byproductQty: number; | byproductQty: number; | ||||
| byproductUom: string; | byproductUom: string; | ||||
| productProcessIssueId: number; | |||||
| productProcessIssueStatus: string; | |||||
| } | } | ||||
| export interface JobOrderLineInfo { | export interface JobOrderLineInfo { | ||||
| id: number, | id: number, | ||||
| @@ -453,6 +455,90 @@ export interface JobTypeResponse { | |||||
| id: number; | id: number; | ||||
| name: string; | 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) => { | export const deleteJobOrder=cache(async (jobOrderId: number) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`, | `${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) => { | export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => { | ||||
| return serverFetchJson<any>( | |||||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||||
| `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`, | `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -568,7 +654,7 @@ export const startProductProcessLine = async (lineId: number) => { | |||||
| }; | }; | ||||
| export const completeProductProcessLine = async (lineId: number) => { | export const completeProductProcessLine = async (lineId: number) => { | ||||
| return serverFetchJson<any>( | return serverFetchJson<any>( | ||||
| `${BASE_API_URL}/product-process/demo/ProcessLine/complete/${lineId}`, | |||||
| `${BASE_API_URL}/product-process/Demo/ProcessLine/complete/${lineId}`, | |||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| headers: { "Content-Type": "application/json" }, | headers: { "Content-Type": "application/json" }, | ||||
| @@ -715,7 +801,7 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe | |||||
| // 获取 Job Order 分层数据 | // 获取 Job Order 分层数据 | ||||
| export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { | ||||
| return serverFetchJson<any>( | |||||
| return serverFetchJson<JobOrderLotsHierarchicalResponse>( | |||||
| `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, | `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -30,6 +30,8 @@ export interface JobOrder { | |||||
| type: string; | type: string; | ||||
| jobTypeId: number; | jobTypeId: number; | ||||
| jobTypeName: string; | jobTypeName: string; | ||||
| sufficientCount: number; | |||||
| insufficientCount: number; | |||||
| // TODO pack below into StockInLineInfo | // TODO pack below into StockInLineInfo | ||||
| stockInLineId?: number; | stockInLineId?: number; | ||||
| stockInLineStatus?: string; | 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) => { | export const fetchAllPickOrderDetails = cache(async (userId?: number) => { | ||||
| if (!userId) { | if (!userId) { | ||||
| return { | return { | ||||
| @@ -25,7 +25,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| const [isAssigning, setIsAssigning] = useState(false); | const [isAssigning, setIsAssigning] = useState(false); | ||||
| //const [selectedDate, setSelectedDate] = useState<string>("today"); | //const [selectedDate, setSelectedDate] = useState<string>("today"); | ||||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | const [selectedDate, setSelectedDate] = useState<string>("today"); | ||||
| const [releaseType, setReleaseType] = useState<string>("batch"); | |||||
| const loadSummaries = useCallback(async () => { | const loadSummaries = useCallback(async () => { | ||||
| setIsLoadingSummary(true); | setIsLoadingSummary(true); | ||||
| try { | try { | ||||
| @@ -40,8 +40,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| } | } | ||||
| const [s2, s4] = await Promise.all([ | 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); | setSummary2F(s2); | ||||
| setSummary4F(s4); | setSummary4F(s4); | ||||
| @@ -50,7 +50,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| } finally { | } finally { | ||||
| setIsLoadingSummary(false); | setIsLoadingSummary(false); | ||||
| } | } | ||||
| }, [selectedDate]); | |||||
| }, [selectedDate, releaseType]); | |||||
| // 初始化 | // 初始化 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -168,10 +168,34 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| {t("Day After Tomorrow")} ({getDateLabel(2)}) | {t("Day After Tomorrow")} ({getDateLabel(2)}) | ||||
| </MenuItem> | </MenuItem> | ||||
| </Select> | </Select> | ||||
| </FormControl> | </FormControl> | ||||
| </Box> | </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 | <Box | ||||
| sx={{ | sx={{ | ||||
| p: 1, | p: 1, | ||||
| @@ -186,6 +210,13 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| {t("EDT - Lane Code (Unassigned/Total)")} | {t("EDT - Lane Code (Unassigned/Total)")} | ||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Stack> | |||||
| <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}> | |||||
| </Stack> | </Stack> | ||||
| @@ -391,6 +422,11 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| </Stack> | </Stack> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -120,20 +120,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| }; | }; | ||||
| const getStockCounts = (jo: JobOrder) => { | 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 { | 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); | const stockCounts = getStockCounts(row); | ||||
| return ( | return ( | ||||
| <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}> | <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}> | ||||
| {stockCounts.sufficient}/{stockCounts.total} | |||||
| {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient} | |||||
| </span> | </span> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -229,17 +219,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| // onClick: (record) => onDetailClick(record), | // onClick: (record) => onDetailClick(record), | ||||
| // buttonIcon: <EditNote />, | // buttonIcon: <EditNote />, | ||||
| renderCell: (row) => { | renderCell: (row) => { | ||||
| const btnSx = getButtonSx(row); | |||||
| //const btnSx = getButtonSx(row); | |||||
| return ( | return ( | ||||
| <Button | <Button | ||||
| id="emailSupplier" | id="emailSupplier" | ||||
| type="button" | type="button" | ||||
| variant="contained" | variant="contained" | ||||
| color="primary" | 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"} | // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"} | ||||
| onClick={() => onDetailClick(row)} | 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 [openModal, setOpenModal] = useState<boolean>(false); | ||||
| const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | const [modalInfo, setModalInfo] = useState<StockInLineInput>(); | ||||
| /* | |||||
| const onDetailClick = useCallback((record: JobOrder) => { | const onDetailClick = useCallback((record: JobOrder) => { | ||||
| if (record.status == "processing") { | if (record.status == "processing") { | ||||
| @@ -367,7 +360,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||||
| router.push(`/jo/edit?id=${record.id}`) | router.push(`/jo/edit?id=${record.id}`) | ||||
| } | } | ||||
| }, []) | }, []) | ||||
| */ | |||||
| const onDetailClick = useCallback((record: JobOrder) => { | |||||
| router.push(`/jo/edit?id=${record.id}`) | |||||
| }, []) | |||||
| const closeNewModal = useCallback(() => { | const closeNewModal = useCallback(() => { | ||||
| // const response = updateJo({ id: 1, status: "storing" }); | // const response = updateJo({ id: 1, status: "storing" }); | ||||
| setOpenModal(false); // Close the modal first | setOpenModal(false); // Close the modal first | ||||
| @@ -34,13 +34,19 @@ import { | |||||
| checkPickOrderCompletion, | checkPickOrderCompletion, | ||||
| PickOrderCompletionResponse, | PickOrderCompletionResponse, | ||||
| checkAndCompletePickOrderByConsoCode, | checkAndCompletePickOrderByConsoCode, | ||||
| confirmLotSubstitution | |||||
| confirmLotSubstitution, | |||||
| updateStockOutLineStatusByQRCodeAndLotNo, | |||||
| batchSubmitList, | |||||
| batchSubmitListRequest, | |||||
| batchSubmitListLineRequest, | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| // 修改:使用 Job Order API | // 修改:使用 Job Order API | ||||
| import { | import { | ||||
| fetchJobOrderLotsHierarchical, | fetchJobOrderLotsHierarchical, | ||||
| fetchUnassignedJobOrderPickOrders, | fetchUnassignedJobOrderPickOrders, | ||||
| assignJobOrderPickOrder | |||||
| assignJobOrderPickOrder, | |||||
| updateJo, | |||||
| JobOrderLotsHierarchicalResponse, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import { | import { | ||||
| @@ -322,14 +328,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null); | |||||
| // 修改:使用 Job Order 数据结构 | // 修改:使用 Job Order 数据结构 | ||||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||||
| const [filteredLotData, setFilteredLotData] = useState<any[]>([]); | |||||
| // 添加未分配订单状态 | |||||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | ||||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | ||||
| @@ -375,6 +380,60 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | ||||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | 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 订单 | // 修改:加载未分配的 Job Order 订单 | ||||
| const loadUnassignedOrders = useCallback(async () => { | const loadUnassignedOrders = useCallback(async () => { | ||||
| setIsLoadingUnassigned(true); | setIsLoadingUnassigned(true); | ||||
| @@ -466,113 +525,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| try { | try { | ||||
| const userIdToUse = userId || currentUserId; | const userIdToUse = userId || currentUserId; | ||||
| console.log(" fetchJobOrderData called with userId:", userIdToUse); | |||||
| if (!userIdToUse) { | if (!userIdToUse) { | ||||
| console.warn("⚠️ No userId available, skipping API call"); | console.warn("⚠️ No userId available, skipping API call"); | ||||
| setJobOrderData(null); | setJobOrderData(null); | ||||
| setCombinedLotData([]); | |||||
| setOriginalCombinedData([]); | |||||
| return; | return; | ||||
| } | } | ||||
| window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | ||||
| detail: { | detail: { | ||||
| hasData: false, | hasData: false, | ||||
| tabIndex: 0 | tabIndex: 0 | ||||
| } | } | ||||
| })); | })); | ||||
| // 使用 Job Order API | |||||
| // 直接使用类型化的响应 | |||||
| const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); | const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); | ||||
| console.log(" Job Order data:", jobOrderData); | |||||
| console.log(" Job Order data (hierarchical):", jobOrderData); | |||||
| setJobOrderData(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', { | window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { | ||||
| detail: { | detail: { | ||||
| hasData: hasData, | hasData: hasData, | ||||
| tabIndex: 0 | tabIndex: 0 | ||||
| } | } | ||||
| })); | })); | ||||
| // 计算完成状态并发送事件 | |||||
| const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => | |||||
| // 计算完成状态 | |||||
| const allCompleted = allLots.length > 0 && allLots.every((lot) => | |||||
| lot.processingStatus === 'completed' | lot.processingStatus === 'completed' | ||||
| ); | ); | ||||
| // 发送完成状态事件,包含标签页信息 | |||||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | ||||
| detail: { | detail: { | ||||
| allLotsCompleted: allCompleted, | allLotsCompleted: allCompleted, | ||||
| tabIndex: 0 // 明确指定这是来自标签页 0 的事件 | |||||
| tabIndex: 0 | |||||
| } | } | ||||
| })); | })); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("❌ Error fetching job order data:", error); | console.error("❌ Error fetching job order data:", error); | ||||
| setJobOrderData(null); | setJobOrderData(null); | ||||
| setCombinedLotData([]); | |||||
| setOriginalCombinedData([]); | |||||
| // 如果加载失败,禁用打印按钮 | |||||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | ||||
| detail: { | detail: { | ||||
| allLotsCompleted: false, | allLotsCompleted: false, | ||||
| @@ -582,7 +581,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } finally { | } finally { | ||||
| setCombinedDataLoading(false); | setCombinedDataLoading(false); | ||||
| } | } | ||||
| }, [currentUserId]); | |||||
| }, [currentUserId, getAllLotsFromHierarchical]); | |||||
| // 修改:初始化时加载数据 | // 修改:初始化时加载数据 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -828,28 +827,70 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| setIsConfirmingLot(false); | setIsConfirmingLot(false); | ||||
| } | } | ||||
| }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); | }, [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) => { | const processOutsideQrCode = useCallback(async (latestQr: string) => { | ||||
| // Don't process if confirmation modal is open | |||||
| // Don't process if confirmation modal is open | |||||
| if (lotConfirmationOpen) { | if (lotConfirmationOpen) { | ||||
| console.log("⏸️ Confirmation modal is open, skipping QR processing"); | console.log("⏸️ Confirmation modal is open, skipping QR processing"); | ||||
| return; | return; | ||||
| } | } | ||||
| // 1) Parse JSON safely | |||||
| let qrData: any = null; | let qrData: any = null; | ||||
| try { | try { | ||||
| qrData = JSON.parse(latestQr); | qrData = JSON.parse(latestQr); | ||||
| } catch { | } 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; | return; | ||||
| } | } | ||||
| try { | try { | ||||
| // Only use the new API when we have JSON with stockInLineId + itemId | // Only use the new API when we have JSON with stockInLineId + itemId | ||||
| if (!(qrData?.stockInLineId && qrData?.itemId)) { | if (!(qrData?.stockInLineId && qrData?.itemId)) { | ||||
| @@ -859,18 +900,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return; | 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 | // Call new analyze-qr-code API | ||||
| const analysis = await analyzeQrCode({ | const analysis = await analyzeQrCode({ | ||||
| itemId: qrData.itemId, | itemId: qrData.itemId, | ||||
| @@ -892,13 +921,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } = analysis || {}; | } = analysis || {}; | ||||
| // 1) Find all lots for the same item from current expected list | // 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.itemId && analyzedItemId && l.itemId === analyzedItemId) || | ||||
| (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) | (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) | ||||
| ); | ); | ||||
| if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { | if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { | ||||
| // Case 3: No item code match | |||||
| console.error("No item match in expected lots for scanned code"); | console.error("No item match in expected lots for scanned code"); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -906,7 +934,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| // Find the ACTIVE suggested lot (not rejected lots) | // Find the ACTIVE suggested lot (not rejected lots) | ||||
| const activeSuggestedLots = sameItemLotsInExpected.filter(lot => | |||||
| const activeSuggestedLots = sameItemLotsInExpected.filter((lot: any) => | |||||
| lot.lotAvailability !== 'rejected' && | lot.lotAvailability !== 'rejected' && | ||||
| lot.stockOutLineStatus !== 'rejected' && | lot.stockOutLineStatus !== 'rejected' && | ||||
| lot.stockOutLineStatus !== 'completed' | lot.stockOutLineStatus !== 'completed' | ||||
| @@ -919,21 +947,78 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return; | 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]; | 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); | await handleQrCodeSubmit(scanned.lotNo); | ||||
| return; | return; | ||||
| } | } | ||||
| // Case 2: Same item, different lot - show confirmation modal | |||||
| console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | ||||
| // DON'T stop scanning - just pause QR processing by showing modal | |||||
| setSelectedLotForQr(expectedLot); | setSelectedLotForQr(expectedLot); | ||||
| handleLotMismatch( | handleLotMismatch( | ||||
| { | { | ||||
| @@ -955,7 +1040,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| return; | return; | ||||
| } | } | ||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]); | |||||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, fetchJobOrderData]); | |||||
| const handleManualInputSubmit = useCallback(() => { | const handleManualInputSubmit = useCallback(() => { | ||||
| @@ -1229,7 +1314,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | }, [fetchJobOrderData, checkAndAutoAssignNext]); | ||||
| const handleSubmitAllScanned = useCallback(async () => { | const handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | 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) { | if (scannedLots.length === 0) { | ||||
| @@ -1238,62 +1323,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| setIsSubmittingAll(true); | setIsSubmittingAll(true); | ||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`); | |||||
| try { | 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 currentActualPickQty = lot.actualPickQty || 0; | ||||
| const cumulativeQty = currentActualPickQty + submitQty; | const cumulativeQty = currentActualPickQty + submitQty; | ||||
| let newStatus = 'partially_completed'; | let newStatus = 'partially_completed'; | ||||
| if (cumulativeQty >= lot.requiredQty) { | |||||
| if (cumulativeQty >= (lot.requiredQty || 0)) { | |||||
| newStatus = 'completed'; | 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); | setQrScanSuccess(true); | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| checkAndAutoAssignNext(); | checkAndAutoAssignNext(); | ||||
| }, 2000); | }, 2000); | ||||
| } else { | |||||
| console.error("Batch submit failed:", result); | |||||
| setQrScanError(true); | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| @@ -1302,7 +1378,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } finally { | } finally { | ||||
| setIsSubmittingAll(false); | setIsSubmittingAll(false); | ||||
| } | } | ||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]); | |||||
| // Calculate scanned items count | // Calculate scanned items count | ||||
| const scannedItemsCount = useMemo(() => { | 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) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| setPaginationController(prev => ({ | setPaginationController(prev => ({ | ||||
| @@ -34,7 +34,11 @@ import { | |||||
| checkPickOrderCompletion, | checkPickOrderCompletion, | ||||
| PickOrderCompletionResponse, | PickOrderCompletionResponse, | ||||
| checkAndCompletePickOrderByConsoCode, | checkAndCompletePickOrderByConsoCode, | ||||
| confirmLotSubstitution | |||||
| confirmLotSubstitution, | |||||
| updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加 | |||||
| batchSubmitList, // ✅ 添加 | |||||
| batchSubmitListRequest, // ✅ 添加 | |||||
| batchSubmitListLineRequest, | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| // 修改:使用 Job Order API | // 修改:使用 Job Order API | ||||
| import { | import { | ||||
| @@ -42,7 +46,8 @@ import { | |||||
| //fetchUnassignedJobOrderPickOrders, | //fetchUnassignedJobOrderPickOrders, | ||||
| assignJobOrderPickOrder, | assignJobOrderPickOrder, | ||||
| fetchJobOrderLotsHierarchicalByPickOrderId, | fetchJobOrderLotsHierarchicalByPickOrderId, | ||||
| updateJoPickOrderHandledBy | |||||
| updateJoPickOrderHandledBy, | |||||
| JobOrderLotsHierarchicalResponse, | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import { | import { | ||||
| @@ -326,11 +331,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| // 修改:使用 Job Order 数据结构 | // 修改:使用 Job Order 数据结构 | ||||
| const [jobOrderData, setJobOrderData] = useState<any>(null); | |||||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | const [combinedDataLoading, setCombinedDataLoading] = useState(false); | ||||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||||
| // 添加未分配订单状态 | // 添加未分配订单状态 | ||||
| const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); | ||||
| const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); | ||||
| @@ -343,7 +346,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const [qrScanInput, setQrScanInput] = useState<string>(''); | const [qrScanInput, setQrScanInput] = useState<string>(''); | ||||
| const [qrScanError, setQrScanError] = useState<boolean>(false); | const [qrScanError, setQrScanError] = useState<boolean>(false); | ||||
| const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); | ||||
| const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null); | |||||
| const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | 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 [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | ||||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | 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 订单 | // 修改:加载未分配的 Job Order 订单 | ||||
| const loadUnassignedOrders = useCallback(async () => { | const loadUnassignedOrders = useCallback(async () => { | ||||
| setIsLoadingUnassigned(true); | setIsLoadingUnassigned(true); | ||||
| @@ -467,120 +515,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| if (!pickOrderId) { | if (!pickOrderId) { | ||||
| console.warn("⚠️ No pickOrderId provided, skipping API call"); | console.warn("⚠️ No pickOrderId provided, skipping API call"); | ||||
| setJobOrderData(null); | setJobOrderData(null); | ||||
| setCombinedLotData([]); | |||||
| setOriginalCombinedData([]); | |||||
| return; | 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); | const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId); | ||||
| console.log("✅ Job Order data:", jobOrderData); | |||||
| console.log("✅ Job Order data (hierarchical):", jobOrderData); | |||||
| setJobOrderData(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) { | } catch (error) { | ||||
| console.error("❌ Error fetching job order data:", error); | console.error("❌ Error fetching job order data:", error); | ||||
| setJobOrderData(null); | setJobOrderData(null); | ||||
| setCombinedLotData([]); | |||||
| setOriginalCombinedData([]); | |||||
| window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { | |||||
| detail: { | |||||
| allLotsCompleted: false, | |||||
| tabIndex: 0 | |||||
| } | |||||
| })); | |||||
| } finally { | } finally { | ||||
| setCombinedDataLoading(false); | setCombinedDataLoading(false); | ||||
| } | } | ||||
| }, []); | |||||
| }, [getAllLotsFromHierarchical]); | |||||
| const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { | ||||
| if (!currentUserId || !pickOrderId || !itemId) { | if (!currentUserId || !pickOrderId || !itemId) { | ||||
| return; | return; | ||||
| @@ -796,7 +751,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | pickOrderLineId: selectedLotForQr.pickOrderLineId, | ||||
| stockOutLineId: selectedLotForQr.stockOutLineId, | stockOutLineId: selectedLotForQr.stockOutLineId, | ||||
| originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, | originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, | ||||
| newInventoryLotLineId: newLotLineId | |||||
| newInventoryLotNo: scannedLotData.lotNo | |||||
| }); | }); | ||||
| console.log(" Lot substitution result:", substitutionResult); | console.log(" Lot substitution result:", substitutionResult); | ||||
| @@ -947,10 +902,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| // 2) Check if the scanned lot matches exactly | // 2) Check if the scanned lot matches exactly | ||||
| if (scanned?.lotNo === expectedLot.lotNo) { | 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 | // Case 2: Same item, different lot - show confirmation modal | ||||
| @@ -1255,7 +1253,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }, [fetchJobOrderData, checkAndAutoAssignNext]); | }, [fetchJobOrderData, checkAndAutoAssignNext]); | ||||
| const handleSubmitAllScanned = useCallback(async () => { | const handleSubmitAllScanned = useCallback(async () => { | ||||
| const scannedLots = combinedLotData.filter(lot => | 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) { | if (scannedLots.length === 0) { | ||||
| @@ -1264,94 +1262,54 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| setIsSubmittingAll(true); | setIsSubmittingAll(true); | ||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`); | |||||
| console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`); | |||||
| try { | 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 currentActualPickQty = lot.actualPickQty || 0; | ||||
| const cumulativeQty = currentActualPickQty + submitQty; | const cumulativeQty = currentActualPickQty + submitQty; | ||||
| let newStatus = 'partially_completed'; | let newStatus = 'partially_completed'; | ||||
| if (cumulativeQty >= lot.requiredQty) { | |||||
| if (cumulativeQty >= (lot.requiredQty || 0)) { | |||||
| newStatus = 'completed'; | 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; | const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; | ||||
| await fetchJobOrderData(pickOrderId); | await fetchJobOrderData(pickOrderId); | ||||
| if (successCount > 0) { | |||||
| if (result && result.code === "SUCCESS") { | |||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| checkAndAutoAssignNext(); | checkAndAutoAssignNext(); | ||||
| }, 2000); | }, 2000); | ||||
| } else { | |||||
| console.error("Batch submit failed:", result); | |||||
| setQrScanError(true); | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| @@ -1360,7 +1318,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } finally { | } finally { | ||||
| setIsSubmittingAll(false); | setIsSubmittingAll(false); | ||||
| } | } | ||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]); | |||||
| }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId]) | |||||
| // Calculate scanned items count | // Calculate scanned items count | ||||
| const scannedItemsCount = useMemo(() => { | 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) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| setPaginationController(prev => ({ | setPaginationController(prev => ({ | ||||
| @@ -620,6 +620,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| const isCompleted = statusLower === 'completed'; | const isCompleted = statusLower === 'completed'; | ||||
| const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; | const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; | ||||
| const isPaused = statusLower === 'paused'; | |||||
| const isPending = statusLower === 'pending' || status === ''; | const isPending = statusLower === 'pending' || status === ''; | ||||
| return ( | return ( | ||||
| @@ -657,6 +658,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| }} /> | }} /> | ||||
| ) : isPending ? ( | ) : isPending ? ( | ||||
| <Chip label={t("Pending")} color="default" size="small" /> | <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" /> | <Chip label={t("Unknown")} color="error" size="small" /> | ||||
| )} | )} | ||||
| @@ -672,7 +675,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { | |||||
| > | > | ||||
| {t("Start")} | {t("Start")} | ||||
| </Button> | </Button> | ||||
| ) : statusLower === 'in_progress' || statusLower === 'in progress' ? ( | |||||
| ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| @@ -131,7 +131,7 @@ const isStockSufficient = (line: JobOrderLine) => { | |||||
| const stockCounts = useMemo(() => { | const stockCounts = useMemo(() => { | ||||
| // 过滤掉 consumables 类型的 lines | // 过滤掉 consumables 类型的 lines | ||||
| const nonConsumablesLines = jobOrderLines.filter( | const nonConsumablesLines = jobOrderLines.filter( | ||||
| line => line.type?.toLowerCase() !== "consumables" | |||||
| line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb" | |||||
| ); | ); | ||||
| const total = nonConsumablesLines.length; | const total = nonConsumablesLines.length; | ||||
| const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | const sufficient = nonConsumablesLines.filter(isStockSufficient).length; | ||||
| @@ -334,7 +334,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") { | |||||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||||
| return t("N/A"); | return t("N/A"); | ||||
| } | } | ||||
| @@ -350,7 +350,7 @@ 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") { | |||||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||||
| return t("N/A"); | return t("N/A"); | ||||
| } | } | ||||
| const stockAvailable = getStockAvailable(params.row); | const stockAvailable = getStockAvailable(params.row); | ||||
| @@ -386,7 +386,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") { | |||||
| if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") { | |||||
| return <Typography>{t("N/A")}</Typography>; | return <Typography>{t("N/A")}</Typography>; | ||||
| } | } | ||||
| return isStockSufficient(params.row) | return isStockSufficient(params.row) | ||||
| @@ -22,6 +22,9 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import { | import { | ||||
| fetchAllJoborderProductProcessInfo, | fetchAllJoborderProductProcessInfo, | ||||
| AllJoborderProductProcessInfoResponse, | AllJoborderProductProcessInfoResponse, | ||||
| updateJo, | |||||
| fetchProductProcessesByJobOrderId, | |||||
| completeProductProcessLine | |||||
| } from "@/app/api/jo/actions"; | } from "@/app/api/jo/actions"; | ||||
| import { StockInLineInput } from "@/app/api/stockIn"; | import { StockInLineInput } from "@/app/api/stockIn"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| @@ -55,6 +58,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| }); | }); | ||||
| setOpenModal(true); | setOpenModal(true); | ||||
| }, [t]); | }, [t]); | ||||
| const fetchProcesses = useCallback(async () => { | const fetchProcesses = useCallback(async () => { | ||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| @@ -72,12 +76,48 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchProcesses(); | fetchProcesses(); | ||||
| }, [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 closeNewModal = useCallback(() => { | ||||
| // const response = updateJo({ id: 1, status: "storing" }); | // const response = updateJo({ id: 1, status: "storing" }); | ||||
| setOpenModal(false); // Close the modal first | setOpenModal(false); // Close the modal first | ||||
| // setTimeout(() => { | // setTimeout(() => { | ||||
| // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | ||||
| }, []); | }, []); | ||||
| const startIdx = page * PER_PAGE; | const startIdx = page * PER_PAGE; | ||||
| const paged = processes.slice(startIdx, startIdx + PER_PAGE); | const paged = processes.slice(startIdx, startIdx + PER_PAGE); | ||||
| @@ -104,10 +144,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| ? "primary" | ? "primary" | ||||
| : "default"; | : "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 totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0; | ||||
| const linesWithStatus = (process.lines || []).filter( | 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)}> | <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}> | ||||
| {t("View Details")} | {t("View Details")} | ||||
| </Button> | </Button> | ||||
| {statusLower !== "completed" && ( | |||||
| <Button variant="contained" size="small" onClick={() => handleUpdateJo(process)}> | |||||
| {t("Update Job Order")} | |||||
| </Button> | |||||
| )} | |||||
| {statusLower === "completed" && ( | {statusLower === "completed" && ( | ||||
| <Button onClick={() => handleViewStockIn(process)}> | <Button onClick={() => handleViewStockIn(process)}> | ||||
| {t("view stockin")} | {t("view stockin")} | ||||
| @@ -11,6 +11,10 @@ import { | |||||
| TableCell, | TableCell, | ||||
| TableHead, | TableHead, | ||||
| TableRow, | TableRow, | ||||
| Dialog, | |||||
| DialogTitle, | |||||
| DialogContent, | |||||
| DialogActions, | |||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| Grid, | Grid, | ||||
| @@ -21,7 +25,7 @@ import StopIcon from "@mui/icons-material/Stop"; | |||||
| import PauseIcon from "@mui/icons-material/Pause"; | import PauseIcon from "@mui/icons-material/Pause"; | ||||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | ||||
| import { useTranslation } from "react-i18next"; | 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 { Operator, Machine } from "@/app/api/jo"; | ||||
| import React, { useCallback, useEffect, useState } from "react"; | import React, { useCallback, useEffect, useState } from "react"; | ||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| @@ -36,7 +40,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| lineId, | lineId, | ||||
| onBack, | onBack, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | |||||
| const { t } = useTranslation( ["common","jo"]); | |||||
| const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); | ||||
| const isCompleted = lineDetail?.status === "Completed"; | const isCompleted = lineDetail?.status === "Completed"; | ||||
| const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { | ||||
| @@ -71,6 +75,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; | ||||
| const [remainingTime, setRemainingTime] = useState<string | null>(null); | const [remainingTime, setRemainingTime] = useState<string | null>(null); | ||||
| const[isOpenReasonModel, setIsOpenReasonModel] = useState(false); | |||||
| const [pauseReason, setPauseReason] = useState(""); | |||||
| // 检查是否两个都已扫描 | // 检查是否两个都已扫描 | ||||
| //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; | ||||
| @@ -126,6 +132,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| const timer = setInterval(update, 1000); | const timer = setInterval(update, 1000); | ||||
| return () => clearInterval(timer); | return () => clearInterval(timer); | ||||
| }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); | }, [lineDetail?.durationInMinutes, lineDetail?.startTime]); | ||||
| const handleSubmitOutput = async () => { | const handleSubmitOutput = async () => { | ||||
| if (!lineDetail?.id) return; | 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 ( | return ( | ||||
| <Box> | <Box> | ||||
| <Box sx={{ mb: 2 }}> | <Box sx={{ mb: 2 }}> | ||||
| @@ -407,16 +433,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| variant="contained" | variant="contained" | ||||
| color="error" | color="error" | ||||
| startIcon={<StopIcon />} | startIcon={<StopIcon />} | ||||
| onClick={handleStop} | |||||
| onClick={() => saveProductProcessIssueTime(lineDetail?.id || 0 as number)} | |||||
| > | > | ||||
| {t("Stop")} | {t("Stop")} | ||||
| </Button> | </Button> | ||||
| {!isPaused ? ( | |||||
| */ | |||||
| } | |||||
| { lineDetail?.status === 'InProgress'? ( | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="warning" | color="warning" | ||||
| startIcon={<PauseIcon />} | startIcon={<PauseIcon />} | ||||
| onClick={handlePause} | |||||
| onClick={() => handleOpenReasonModel()} | |||||
| > | > | ||||
| {t("Pause")} | {t("Pause")} | ||||
| </Button> | </Button> | ||||
| @@ -425,12 +453,12 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro | |||||
| variant="contained" | variant="contained" | ||||
| color="success" | color="success" | ||||
| startIcon={<PlayArrowIcon />} | startIcon={<PlayArrowIcon />} | ||||
| onClick={handleContinue} | |||||
| onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)} | |||||
| > | > | ||||
| {t("Continue")} | {t("Continue")} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| */} | |||||
| <Button | <Button | ||||
| sx={{ mt: 2, alignSelf: "flex-end" }} | sx={{ mt: 2, alignSelf: "flex-end" }} | ||||
| variant="outlined" | 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> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -13,6 +13,8 @@ | |||||
| "code": "編號", | "code": "編號", | ||||
| "Name": "名稱", | "Name": "名稱", | ||||
| "Type": "類型", | "Type": "類型", | ||||
| "Update Job Order": "更新工單", | |||||
| "No": "沒有", | |||||
| "WIP": "半成品", | "WIP": "半成品", | ||||
| "R&D": "研發", | "R&D": "研發", | ||||
| @@ -195,10 +197,16 @@ | |||||
| "Seq No": "加入步驟", | "Seq No": "加入步驟", | ||||
| "Seq No Remark": "序號明細", | "Seq No Remark": "序號明細", | ||||
| "Stock Available": "庫存可用", | "Stock Available": "庫存可用", | ||||
| "Confirm": "確認", | |||||
| "Stock Status": "庫存狀態", | "Stock Status": "庫存狀態", | ||||
| "Target Production Date": "目標生產日期", | "Target Production Date": "目標生產日期", | ||||
| "id": "ID", | "id": "ID", | ||||
| "Finished lines": "完成行", | |||||
| "Finished lines": "已完成流程", | |||||
| "Please scan staff no": "請掃描員工編號", | |||||
| "Paused": "已暫停", | |||||
| "paused": "已暫停", | |||||
| "Pause Reason": "暫停原因", | |||||
| "Reason": "原因", | |||||
| "Invalid Stock In Line Id": "無效庫存行ID", | "Invalid Stock In Line Id": "無效庫存行ID", | ||||
| "Production date": "生產日期", | "Production date": "生產日期", | ||||
| "Required Qty": "需求數量", | "Required Qty": "需求數量", | ||||
| @@ -10,6 +10,7 @@ | |||||
| "Picked Qty": "已提料數量", | "Picked Qty": "已提料數量", | ||||
| "Req. Qty": "需求數量", | "Req. Qty": "需求數量", | ||||
| "UoM": "銷售單位", | "UoM": "銷售單位", | ||||
| "No": "沒有", | |||||
| "Status": "工單狀態", | "Status": "工單狀態", | ||||
| "Lot No.": "批號", | "Lot No.": "批號", | ||||
| "Delete Job Order": "刪除工單", | "Delete Job Order": "刪除工單", | ||||
| @@ -40,7 +41,13 @@ | |||||
| "Production Priority": "生產優先度", | "Production Priority": "生產優先度", | ||||
| "Sequence": "序", | "Sequence": "序", | ||||
| "Item Code": "成品/半成品編號", | "Item Code": "成品/半成品編號", | ||||
| "Paused": "已暫停", | |||||
| "paused": "已暫停", | |||||
| "Pause Reason": "暫停原因", | |||||
| "Reason": "原因", | |||||
| "Stock Available": "倉庫可用數", | "Stock Available": "倉庫可用數", | ||||
| "Staff No": "員工編號", | |||||
| "Please scan staff no": "請掃描員工編號", | |||||
| "Stock Status": "可提料", | "Stock Status": "可提料", | ||||
| "Total lines: ": "所需貨品項目數量: ", | "Total lines: ": "所需貨品項目數量: ", | ||||
| "Lines with sufficient stock: ": "可提料項目數量: ", | "Lines with sufficient stock: ": "可提料項目數量: ", | ||||