| @@ -77,13 +77,21 @@ export const fetchLotDetail = cache(async (stockInLineId: number) => { | |||||
| export const updateInventoryLotLineStatus = async (data: { | export const updateInventoryLotLineStatus = async (data: { | ||||
| inventoryLotLineId: number; | inventoryLotLineId: number; | ||||
| status: string; | status: string; | ||||
| qty: number; | |||||
| operation?: string; | |||||
| //qty: number; | |||||
| //operation?: string; | |||||
| }) => { | }) => { | ||||
| return serverFetchJson(`${BASE_API_URL}/inventory/lot-line/update-status`, { | |||||
| method: 'PUT', | |||||
| body: JSON.stringify(data) | |||||
| // return await serverFetchJson(`${BASE_API_URL}/inventoryLotLine/updateStatus`, { | |||||
| // next: { tags: ["inventoryLotLine"] }, | |||||
| // method: 'POST', | |||||
| // body: JSON.stringify(data) | |||||
| // }); | |||||
| return await serverFetchJson<PostInventoryLotLineResponse<InventoryLotLineResult>>(`${BASE_API_URL}/inventoryLotLine/updateStatus`, { | |||||
| next: { tags: ["inventoryLotLine"] }, | |||||
| method: 'POST', | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | }); | ||||
| // revalidateTag("po"); | |||||
| }; | }; | ||||
| export const fetchInventories = cache(async (data: SearchInventory) => { | export const fetchInventories = cache(async (data: SearchInventory) => { | ||||
| @@ -22,7 +22,8 @@ import { useTranslation } from "react-i18next"; | |||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | import QrCodeIcon from '@mui/icons-material/QrCode'; | ||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | ||||
| import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | ||||
| import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; | |||||
| import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; | |||||
| interface LotPickData { | interface LotPickData { | ||||
| id: number; | id: number; | ||||
| lotId: number; | lotId: number; | ||||
| @@ -63,6 +64,7 @@ interface LotTableProps { | |||||
| setShowInputBody: (show: boolean) => void; | setShowInputBody: (show: boolean) => void; | ||||
| selectedLotForInput: LotPickData | null; | selectedLotForInput: LotPickData | null; | ||||
| generateInputBody: () => any; | generateInputBody: () => any; | ||||
| onDataRefresh: () => Promise<void>; | |||||
| } | } | ||||
| // ✅ QR Code Modal Component | // ✅ QR Code Modal Component | ||||
| @@ -222,6 +224,23 @@ const handleManualSubmit = () => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| */} | */} | ||||
| useEffect(() => { | |||||
| if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '') { | |||||
| // Auto-submit when manual input matches the expected lot number | |||||
| console.log('🔄 Auto-submitting manual input:', manualInput.trim()); | |||||
| // Add a small delay to ensure proper execution order | |||||
| const timer = setTimeout(() => { | |||||
| onQrCodeSubmit(lot.lotNo); | |||||
| onClose(); | |||||
| setManualInput(''); | |||||
| setManualInputError(false); | |||||
| setManualInputSubmitted(false); | |||||
| }, 200); // 200ms delay | |||||
| return () => clearTimeout(timer); | |||||
| } | |||||
| }, [manualInput, lot, onQrCodeSubmit, onClose]); | |||||
| const handleManualSubmit = () => { | const handleManualSubmit = () => { | ||||
| if (manualInput.trim() === lot?.lotNo) { | if (manualInput.trim() === lot?.lotNo) { | ||||
| // ✅ Success - no error helper text needed | // ✅ Success - no error helper text needed | ||||
| @@ -327,6 +346,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| setShowInputBody, | setShowInputBody, | ||||
| selectedLotForInput, | selectedLotForInput, | ||||
| generateInputBody, | generateInputBody, | ||||
| onDataRefresh, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| @@ -347,24 +367,24 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| // ✅ 添加状态消息生成函数 | // ✅ 添加状态消息生成函数 | ||||
| const getStatusMessage = useCallback((lot: LotPickData) => { | const getStatusMessage = useCallback((lot: LotPickData) => { | ||||
| if (!lot.stockOutLineId) { | if (!lot.stockOutLineId) { | ||||
| return "Please finish QR code scan, QC check and pick order."; | |||||
| return t("Please finish QR code scan, QC check and pick order."); | |||||
| } | } | ||||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | switch (lot.stockOutLineStatus?.toLowerCase()) { | ||||
| case 'pending': | case 'pending': | ||||
| return "Please finish QC check and pick order."; | |||||
| return t("Please finish QC check and pick order."); | |||||
| case 'checked': | case 'checked': | ||||
| return "Please submit the pick order."; | |||||
| return t("Please submit the pick order."); | |||||
| case 'partially_completed': | case 'partially_completed': | ||||
| return "Partial quantity submitted. You can submit more or complete the order."; | |||||
| return t("Partial quantity submitted. Please submit more or complete the order.") ; | |||||
| case 'completed': | case 'completed': | ||||
| return "Pick order completed successfully!"; | |||||
| return t("Pick order completed successfully!"); | |||||
| case 'rejected': | case 'rejected': | ||||
| return "QC check failed. Lot has been rejected and marked as unavailable."; | |||||
| return t("QC check failed. Lot has been rejected and marked as unavailable."); | |||||
| case 'unavailable': | case 'unavailable': | ||||
| return "This order is insufficient, please pick another lot."; | |||||
| return t("This order is insufficient, please pick another lot."); | |||||
| default: | default: | ||||
| return "Please finish QR code scan, QC check and pick order."; | |||||
| return t("Please finish QR code scan, QC check and pick order."); | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| @@ -399,21 +419,34 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| }, []); | }, []); | ||||
| // ✅ Handle QR code submission | // ✅ Handle QR code submission | ||||
| const handleQrCodeSubmit = useCallback((lotNo: string) => { | |||||
| const handleQrCodeSubmit = useCallback(async (lotNo: string) => { | |||||
| if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | ||||
| console.log(`✅ QR Code verified for lot: ${lotNo}`); | console.log(`✅ QR Code verified for lot: ${lotNo}`); | ||||
| // ✅ Create stock out line | |||||
| onCreateStockOutLine(selectedLotForQr.lotId); | |||||
| // ✅ Store the required quantity before creating stock out line | |||||
| const requiredQty = selectedLotForQr.requiredQty; | |||||
| const lotId = selectedLotForQr.lotId; | |||||
| // ✅ Show success message | |||||
| console.log("Stock out line created successfully!"); | |||||
| // ✅ Create stock out line and wait for it to complete | |||||
| await onCreateStockOutLine(selectedLotForQr.lotId); | |||||
| // ✅ Close modal | // ✅ Close modal | ||||
| setQrModalOpen(false); | setQrModalOpen(false); | ||||
| setSelectedLotForQr(null); | setSelectedLotForQr(null); | ||||
| // ✅ Set pick quantity AFTER stock out line creation and refresh is complete | |||||
| if (selectedRowId) { | |||||
| // Add a small delay to ensure the data refresh from onCreateStockOutLine is complete | |||||
| setTimeout(() => { | |||||
| onPickQtyChange(selectedRowId, lotId, requiredQty); | |||||
| console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); | |||||
| }, 500); // 500ms delay to ensure refresh is complete | |||||
| } | |||||
| // ✅ Show success message | |||||
| console.log("Stock out line created successfully!"); | |||||
| } | } | ||||
| }, [selectedLotForQr, onCreateStockOutLine]); | |||||
| }, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -520,6 +553,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| </TableCell> | </TableCell> | ||||
| {/* QC Check Button */} | {/* QC Check Button */} | ||||
| {/* | |||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -541,7 +575,8 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| > | > | ||||
| {t("QC")} | {t("QC")} | ||||
| </Button> | </Button> | ||||
| </TableCell> | |||||
| */} | |||||
| {/* Lot Actual Pick Qty */} | {/* Lot Actual Pick Qty */} | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| @@ -585,7 +620,42 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| placeholder="0" // Show placeholder instead of default value | placeholder="0" // Show placeholder instead of default value | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={async () => { | |||||
| if (selectedRowId && selectedRow && lot.stockOutLineId) { | |||||
| try { | |||||
| // ✅ Call updateStockOutLineStatus to reject the stock out line | |||||
| await updateStockOutLineStatus({ | |||||
| id: lot.stockOutLineId, | |||||
| status: 'rejected', | |||||
| qty: 0 | |||||
| }); | |||||
| // ✅ Refresh data after rejection | |||||
| if (onDataRefresh) { | |||||
| await onDataRefresh(); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error rejecting lot:", error); | |||||
| } | |||||
| } | |||||
| }} | |||||
| // ✅ Only enable if stock out line exists | |||||
| disabled={!lot.stockOutLineId} | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '40px' | |||||
| }} | |||||
| > | |||||
| {t("Reject")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| {/* Submit Button */} | {/* Submit Button */} | ||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| <Button | <Button | ||||
| @@ -600,7 +670,7 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || | (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || | ||||
| !pickQtyData[selectedRowId!]?.[lot.lotId] || | !pickQtyData[selectedRowId!]?.[lot.lotId] || | ||||
| !lot.stockOutLineStatus || // Must have stock out line | !lot.stockOutLineStatus || // Must have stock out line | ||||
| !['checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) // Only these statuses | |||||
| !['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) // Only these statuses | |||||
| } | } | ||||
| // ✅ Allow submission for available AND insufficient_stock lots | // ✅ Allow submission for available AND insufficient_stock lots | ||||
| sx={{ | sx={{ | ||||
| @@ -515,6 +515,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| // ✅ Fix lot selection logic | // ✅ Fix lot selection logic | ||||
| const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => { | const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => { | ||||
| console.log("=== DEBUG: Lot Selection ==="); | |||||
| console.log("uniqueLotId:", uniqueLotId); | |||||
| console.log("lotId (inventory lot line ID):", lotId); | |||||
| // Find the selected lot data | |||||
| const selectedLot = lotData.find(lot => lot.lotId === lotId); | |||||
| console.log("Selected lot data:", selectedLot); | |||||
| // If clicking the same lot, unselect it | // If clicking the same lot, unselect it | ||||
| if (selectedLotRowId === uniqueLotId) { | if (selectedLotRowId === uniqueLotId) { | ||||
| setSelectedLotRowId(null); | setSelectedLotRowId(null); | ||||
| @@ -1022,7 +1029,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| {selectedRow && ( | {selectedRow && ( | ||||
| <Box> | <Box> | ||||
| <Typography variant="h6" gutterBottom> | <Typography variant="h6" gutterBottom> | ||||
| Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName} | |||||
| {t("Item lot to be Pick:")} {selectedRow.pickOrderCode} - {selectedRow.itemName} | |||||
| </Typography> | </Typography> | ||||
| {/* 检查是否有可用的批次数据 */} | {/* 检查是否有可用的批次数据 */} | ||||
| @@ -1039,6 +1046,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| onSubmitPickQty={handleSubmitPickQty} | onSubmitPickQty={handleSubmitPickQty} | ||||
| onCreateStockOutLine={handleCreateStockOutLine} | onCreateStockOutLine={handleCreateStockOutLine} | ||||
| onQcCheck={handleQcCheck} | onQcCheck={handleQcCheck} | ||||
| onDataRefresh={handleFetchAllPickOrderDetails} | |||||
| onLotSelectForInput={handleLotSelectForInput} | onLotSelectForInput={handleLotSelectForInput} | ||||
| showInputBody={showInputBody} | showInputBody={showInputBody} | ||||
| setShowInputBody={setShowInputBody} | setShowInputBody={setShowInputBody} | ||||
| @@ -1079,7 +1087,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| onClick={() => handleInsufficientStock()} | onClick={() => handleInsufficientStock()} | ||||
| sx={{ whiteSpace: 'nowrap' }} | sx={{ whiteSpace: 'nowrap' }} | ||||
| > | > | ||||
| {t("Insufficient Stock & Pick Another Lot")} | |||||
| {t("Pick Another Lot")} | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| @@ -228,7 +228,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| // 添加处理提料单创建成功的函数 | // 添加处理提料单创建成功的函数 | ||||
| const handlePickOrderCreated = useCallback(() => { | const handlePickOrderCreated = useCallback(() => { | ||||
| // 切换到 Assign & Release 标签页 (tabIndex = 1) | // 切换到 Assign & Release 标签页 (tabIndex = 1) | ||||
| setTabIndex(1); | |||||
| setTabIndex(2); | |||||
| }, []); | }, []); | ||||
| return ( | return ( | ||||
| @@ -248,6 +248,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| {t("Pick Order")} | {t("Pick Order")} | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| {/* | |||||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | ||||
| <Button onClick={openCreateModal}> | <Button onClick={openCreateModal}> | ||||
| {t("create")} | {t("create")} | ||||
| @@ -260,6 +261,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| /> | /> | ||||
| } | } | ||||
| </Grid> | </Grid> | ||||
| */} | |||||
| </Grid> | </Grid> | ||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| @@ -322,21 +322,41 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| // ✅ QC Decision 2: Report and Re-pick | // ✅ QC Decision 2: Report and Re-pick | ||||
| console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable"); | console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable"); | ||||
| // ✅ Stock out line status: rejected | |||||
| await updateStockOutLineStatus({ | |||||
| id: selectedLotId, | |||||
| status: 'rejected', | |||||
| qty: acceptQty || 0 | |||||
| }); | |||||
| // ✅ Inventory lot line status: unavailable | // ✅ Inventory lot line status: unavailable | ||||
| if (selectedLot) { | if (selectedLot) { | ||||
| await updateInventoryLotLineStatus({ | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable', | |||||
| qty: acceptQty || 0, | |||||
| operation: 'reject' | |||||
| }); | |||||
| try { | |||||
| console.log("=== DEBUG: Updating inventory lot line status ==="); | |||||
| console.log("Selected lot:", selectedLot); | |||||
| console.log("Selected lot ID:", selectedLotId); | |||||
| // ✅ FIX: Only send the fields that the backend expects | |||||
| const updateData = { | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable' | |||||
| // ❌ Remove qty and operation - backend doesn't expect these | |||||
| }; | |||||
| console.log("Update data:", updateData); | |||||
| const result = await updateInventoryLotLineStatus(updateData); | |||||
| console.log("✅ Inventory lot line status updated successfully:", result); | |||||
| } catch (error) { | |||||
| console.error("❌ Error updating inventory lot line status:", error); | |||||
| console.error("Error details:", { | |||||
| selectedLot, | |||||
| selectedLotId, | |||||
| acceptQty | |||||
| }); | |||||
| // Show user-friendly error message | |||||
| return; // Stop execution if this fails | |||||
| } | |||||
| } else { | |||||
| console.error("❌ Selected lot not found for inventory update"); | |||||
| alert("Selected lot not found. Cannot update inventory status."); | |||||
| return; | |||||
| } | } | ||||
| // ✅ Close modal and refresh data | // ✅ Close modal and refresh data | ||||
| @@ -612,16 +632,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| <FormControlLabel | <FormControlLabel | ||||
| value="1" | value="1" | ||||
| control={<Radio />} | control={<Radio />} | ||||
| label="接受出庫" | |||||
| label={t("Accept Stock Out")} | |||||
| /> | /> | ||||
| {/* ✅ Combine options 2 & 3 into one */} | |||||
| {/* ✅ Combirne options 2 & 3 into one */} | |||||
| <FormControlLabel | <FormControlLabel | ||||
| value="2" | value="2" | ||||
| control={<Radio />} | control={<Radio />} | ||||
| sx={{"& .Mui-checked": {color: "blue"}}} | sx={{"& .Mui-checked": {color: "blue"}}} | ||||
| label="上報品檢結果並重新揀貨" | |||||
| label={t("Report and Pick another lot")} | |||||
| /> | /> | ||||
| </RadioGroup> | </RadioGroup> | ||||
| )} | )} | ||||
| @@ -630,15 +650,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| </Grid> | </Grid> | ||||
| {/* ✅ Show escalation component when QC Decision = 2 (Report and Re-pick) */} | {/* ✅ Show escalation component when QC Decision = 2 (Report and Re-pick) */} | ||||
| {qcDecision == 2 && ( | |||||
| <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={false} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> | |||||
| )} | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | <Grid item xs={12} sx={{ mt: 2 }}> | ||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| @@ -1976,8 +1976,8 @@ const CustomSearchResultsTable = () => { | |||||
| )} | )} | ||||
| {/* Add Submit Button between tables */} | {/* Add Submit Button between tables */} | ||||
| {/* Search Results with SearchResults component */} | |||||
| {/* | |||||
| {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( | {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( | ||||
| <Box sx={{ mt: 2, mb: 2 }}> | <Box sx={{ mt: 2, mb: 2 }}> | ||||
| <Box sx={{ display: 'flex', justifyContent: 'left', alignItems: 'center', gap: 2 }}> | <Box sx={{ display: 'flex', justifyContent: 'left', alignItems: 'center', gap: 2 }}> | ||||
| @@ -2001,9 +2001,10 @@ const CustomSearchResultsTable = () => { | |||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| */} | |||||
| {/* 创建项目区域 - 修改Group列为可选择的 */} | {/* 创建项目区域 - 修改Group列为可选择的 */} | ||||
| {createdItems.length > 0 && ( | {createdItems.length > 0 && ( | ||||
| @@ -172,6 +172,16 @@ | |||||
| "Job Order Code": "工單編號", | "Job Order Code": "工單編號", | ||||
| "QC Check": "QC 檢查", | "QC Check": "QC 檢查", | ||||
| "QR Code Scan": "QR Code掃描", | "QR Code Scan": "QR Code掃描", | ||||
| "Pick Order Details": "提料單詳情" | |||||
| "Pick Order Details": "提料單詳情", | |||||
| "Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。", | |||||
| "Pick order completed successfully!": "提料單完成成功!", | |||||
| "QC check failed. Lot has been rejected and marked as unavailable.": "QC 檢查失敗。批號已拒絕並標記為不可用。", | |||||
| "This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。", | |||||
| "Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。", | |||||
| "No data available": "沒有資料", | |||||
| "Please submit the pick order.": "請提交提料單。", | |||||
| "Item lot to be Pick:": "批次貨品提料:", | |||||
| "Report and Pick another lot": "上報並需重新選擇批號", | |||||
| "Accept Stock Out": "接受出庫", | |||||
| "Pick Another Lot": "重新選擇批號" | |||||
| } | } | ||||