| @@ -15,11 +15,13 @@ import { | |||||
| TextField, | TextField, | ||||
| Typography, | Typography, | ||||
| TablePagination, | TablePagination, | ||||
| Modal, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { useCallback, useMemo, useState, useEffect } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | 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'; | |||||
| interface LotPickData { | interface LotPickData { | ||||
| id: number; | id: number; | ||||
| @@ -63,6 +65,164 @@ interface LotTableProps { | |||||
| generateInputBody: () => any; | generateInputBody: () => any; | ||||
| } | } | ||||
| // ✅ QR Code Modal Component | |||||
| const QrCodeModal: React.FC<{ | |||||
| open: boolean; | |||||
| onClose: () => void; | |||||
| lot: LotPickData | null; | |||||
| onQrCodeSubmit: (lotNo: string) => void; | |||||
| }> = ({ open, onClose, lot, onQrCodeSubmit }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||||
| const [manualInput, setManualInput] = useState<string>(''); | |||||
| // ✅ Add state to track manual input submission | |||||
| const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false); | |||||
| const [manualInputError, setManualInputError] = useState<boolean>(false); | |||||
| // ✅ Process scanned QR codes | |||||
| useEffect(() => { | |||||
| if (qrValues.length > 0 && lot) { | |||||
| const latestQr = qrValues[qrValues.length - 1]; | |||||
| const qrContent = latestQr.replace(/[{}]/g, ''); | |||||
| if (qrContent === lot.lotNo) { | |||||
| onQrCodeSubmit(lot.lotNo); | |||||
| onClose(); | |||||
| resetScan(); | |||||
| } else { | |||||
| // ✅ Set error state for helper text | |||||
| setManualInputError(true); | |||||
| setManualInputSubmitted(true); | |||||
| } | |||||
| } | |||||
| }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]); | |||||
| // ✅ Clear states when modal opens or lot changes | |||||
| useEffect(() => { | |||||
| if (open) { | |||||
| setManualInput(''); | |||||
| setManualInputSubmitted(false); | |||||
| setManualInputError(false); | |||||
| } | |||||
| }, [open]); | |||||
| useEffect(() => { | |||||
| if (lot) { | |||||
| setManualInput(''); | |||||
| setManualInputSubmitted(false); | |||||
| setManualInputError(false); | |||||
| } | |||||
| }, [lot]); | |||||
| const handleManualSubmit = () => { | |||||
| if (manualInput.trim() === lot?.lotNo) { | |||||
| // ✅ Success - no error helper text needed | |||||
| onQrCodeSubmit(lot.lotNo); | |||||
| onClose(); | |||||
| setManualInput(''); | |||||
| } else { | |||||
| // ✅ Show error helper text after submit | |||||
| setManualInputError(true); | |||||
| setManualInputSubmitted(true); | |||||
| // Don't clear input - let user see what they typed | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <Modal open={open} onClose={onClose}> | |||||
| <Box sx={{ | |||||
| position: 'absolute', | |||||
| top: '50%', | |||||
| left: '50%', | |||||
| transform: 'translate(-50%, -50%)', | |||||
| bgcolor: 'background.paper', | |||||
| p: 3, | |||||
| borderRadius: 2, | |||||
| minWidth: 400, | |||||
| }}> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| {t("QR Code Scan for Lot")}: {lot?.lotNo} | |||||
| </Typography> | |||||
| {/* QR Scanner Status */} | |||||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||||
| <Typography variant="body2" gutterBottom> | |||||
| <strong>Scanner Status:</strong> {isScanning ? 'Scanning...' : 'Ready'} | |||||
| </Typography> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={isScanning ? stopScan : startScan} | |||||
| size="small" | |||||
| > | |||||
| {isScanning ? 'Stop Scan' : 'Start Scan'} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| onClick={resetScan} | |||||
| size="small" | |||||
| > | |||||
| Reset | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| {/* Manual Input with Submit-Triggered Helper Text */} | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Typography variant="body2" gutterBottom> | |||||
| <strong>Manual Input:</strong> | |||||
| </Typography> | |||||
| <TextField | |||||
| fullWidth | |||||
| size="small" | |||||
| value={manualInput} | |||||
| onChange={(e) => setManualInput(e.target.value)} | |||||
| sx={{ mb: 1 }} | |||||
| // ✅ Only show error after submit button is clicked | |||||
| error={manualInputSubmitted && manualInputError} | |||||
| helperText={ | |||||
| // ✅ Show helper text only after submit with error | |||||
| manualInputSubmitted && manualInputError | |||||
| ? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}` | |||||
| : '' | |||||
| } | |||||
| /> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={handleManualSubmit} | |||||
| disabled={!manualInput.trim()} | |||||
| size="small" | |||||
| color="primary" | |||||
| > | |||||
| Submit Manual Input | |||||
| </Button> | |||||
| </Box> | |||||
| {/* ✅ Show QR Scan Status */} | |||||
| {qrValues.length > 0 && ( | |||||
| <Box sx={{ mb: 2, p: 2, backgroundColor: manualInputError ? '#ffebee' : '#e8f5e8', borderRadius: 1 }}> | |||||
| <Typography variant="body2" color={manualInputError ? 'error' : 'success'}> | |||||
| <strong>QR Scan Result:</strong> {qrValues[qrValues.length - 1]} | |||||
| </Typography> | |||||
| {manualInputError && ( | |||||
| <Typography variant="caption" color="error" display="block"> | |||||
| ❌ Mismatch! Expected: {lot?.lotNo} | |||||
| </Typography> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| <Box sx={{ mt: 2, textAlign: 'right' }}> | |||||
| <Button onClick={onClose} variant="outlined"> | |||||
| Cancel | |||||
| </Button> | |||||
| </Box> | |||||
| </Box> | |||||
| </Modal> | |||||
| ); | |||||
| }; | |||||
| const LotTable: React.FC<LotTableProps> = ({ | const LotTable: React.FC<LotTableProps> = ({ | ||||
| lotData, | lotData, | ||||
| selectedRowId, | selectedRowId, | ||||
| @@ -83,6 +243,14 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| // ✅ Add QR scanner context | |||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||||
| // ✅ Add state for QR input modal | |||||
| const [qrModalOpen, setQrModalOpen] = useState(false); | |||||
| const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null); | |||||
| const [manualQrInput, setManualQrInput] = useState<string>(''); | |||||
| // 分页控制器 | // 分页控制器 | ||||
| const [lotTablePagingController, setLotTablePagingController] = useState({ | const [lotTablePagingController, setLotTablePagingController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| @@ -95,10 +263,10 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| return "Please finish QR code scan, QC check and pick order."; | return "Please finish QR code scan, QC check and pick order."; | ||||
| } | } | ||||
| switch (lot.stockOutLineStatus?.toUpperCase()) { | |||||
| case 'PENDING': | |||||
| switch (lot.stockOutLineStatus?.toLowerCase()) { | |||||
| case 'pending': | |||||
| return "Please finish QC check and pick order."; | return "Please finish QC check and pick order."; | ||||
| case 'COMPLETE': | |||||
| case 'completed': | |||||
| return "Please submit the pick order."; | return "Please submit the pick order."; | ||||
| case 'unavailable': | case 'unavailable': | ||||
| return "This order is insufficient, please pick another lot."; | return "This order is insufficient, please pick another lot."; | ||||
| @@ -137,6 +305,23 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| // ✅ Handle QR code submission | |||||
| const handleQrCodeSubmit = useCallback((lotNo: string) => { | |||||
| if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { | |||||
| console.log(`✅ QR Code verified for lot: ${lotNo}`); | |||||
| // ✅ Create stock out line | |||||
| onCreateStockOutLine(selectedLotForQr.lotId); | |||||
| // ✅ Show success message | |||||
| console.log("Stock out line created successfully!"); | |||||
| // ✅ Close modal | |||||
| setQrModalOpen(false); | |||||
| setSelectedLotForQr(null); | |||||
| } | |||||
| }, [selectedLotForQr, onCreateStockOutLine]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| @@ -198,26 +383,47 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| {/* QR Code Scan Button */} | {/* QR Code Scan Button */} | ||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| onCreateStockOutLine(lot.lotId); | |||||
| onLotSelectForInput(lot); // Show input body when button is clicked | |||||
| }} | |||||
| // ✅ Allow creation for available AND insufficient_stock lots | |||||
| disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || Boolean(lot.stockOutLineId)} | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '40px' | |||||
| }} | |||||
| startIcon={<QrCodeIcon />} // ✅ Add QR code icon | |||||
| > | |||||
| {lot.stockOutLineId ? t("Scanned") : t("Scan")} | |||||
| </Button> | |||||
| <Box sx={{ textAlign: 'center' }}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| setSelectedLotForQr(lot); | |||||
| setQrModalOpen(true); | |||||
| resetScan(); | |||||
| }} | |||||
| // ✅ Disable when: | |||||
| // 1. Lot is expired or unavailable | |||||
| // 2. Already scanned (has stockOutLineId) | |||||
| // 3. Not selected (selectedLotRowId doesn't match) | |||||
| disabled={ | |||||
| (lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || | |||||
| Boolean(lot.stockOutLineId) || | |||||
| selectedLotRowId !== `row_${index}` | |||||
| } | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '40px', | |||||
| // ✅ Visual feedback | |||||
| opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5 | |||||
| }} | |||||
| startIcon={<QrCodeIcon />} | |||||
| title={ | |||||
| selectedLotRowId !== `row_${index}` | |||||
| ? "Please select this lot first to enable QR scanning" | |||||
| : lot.stockOutLineId | |||||
| ? "Already scanned" | |||||
| : "Click to scan QR code" | |||||
| } | |||||
| > | |||||
| {lot.stockOutLineId ? t("Scanned") : t("Scan")} | |||||
| </Button> | |||||
| </Box> | |||||
| </TableCell> | </TableCell> | ||||
| {/* QC Check Button */} | {/* QC Check Button */} | ||||
| @@ -320,6 +526,19 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | ||||
| } | } | ||||
| /> | /> | ||||
| {/* ✅ QR Code Modal */} | |||||
| <QrCodeModal | |||||
| open={qrModalOpen} | |||||
| onClose={() => { | |||||
| setQrModalOpen(false); | |||||
| setSelectedLotForQr(null); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| }} | |||||
| lot={selectedLotForQr} | |||||
| onQrCodeSubmit={handleQrCodeSubmit} | |||||
| /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -490,20 +490,22 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }, [selectedLotRowId]); | }, [selectedLotRowId]); | ||||
| // ✅ Add function to handle row selection that resets lot selection | // ✅ Add function to handle row selection that resets lot selection | ||||
| const handleRowSelect = useCallback(async (lineId: number) => { | |||||
| const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => { | |||||
| setSelectedRowId(lineId); | setSelectedRowId(lineId); | ||||
| // ✅ Reset lot selection when changing pick order line | |||||
| setSelectedLotRowId(null); | |||||
| setSelectedLotId(null); | |||||
| // ✅ Only reset lot selection if not preserving | |||||
| if (!preserveLotSelection) { | |||||
| setSelectedLotRowId(null); | |||||
| setSelectedLotId(null); | |||||
| } | |||||
| try { | try { | ||||
| const lotDetails = await fetchPickOrderLineLotDetails(lineId); | const lotDetails = await fetchPickOrderLineLotDetails(lineId); | ||||
| console.log("Lot details from API:", lotDetails); | console.log("Lot details from API:", lotDetails); | ||||
| const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ | const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ | ||||
| id: lot.lotId, // This is actually ill.id (inventory lot line ID) | |||||
| lotId: lot.lotId, // This should be the unique inventory lot line ID | |||||
| id: lot.id, // This should be the unique row ID for the table | |||||
| lotId: lot.lotId, // This is the inventory lot line ID | |||||
| lotNo: lot.lotNo, | lotNo: lot.lotNo, | ||||
| expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', | expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', | ||||
| location: lot.location, | location: lot.location, | ||||
| @@ -513,7 +515,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| actualPickQty: lot.actualPickQty || 0, | actualPickQty: lot.actualPickQty || 0, | ||||
| lotStatus: lot.lotStatus, | lotStatus: lot.lotStatus, | ||||
| lotAvailability: lot.lotAvailability, | lotAvailability: lot.lotAvailability, | ||||
| // ✅ Add StockOutLine fields | |||||
| stockOutLineId: lot.stockOutLineId, | stockOutLineId: lot.stockOutLineId, | ||||
| stockOutLineStatus: lot.stockOutLineStatus, | stockOutLineStatus: lot.stockOutLineStatus, | ||||
| stockOutLineQty: lot.stockOutLineQty | stockOutLineQty: lot.stockOutLineQty | ||||
| @@ -545,6 +546,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| pickOrderCode: pickOrder.code, | pickOrderCode: pickOrder.code, | ||||
| targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 | targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 | ||||
| balanceToPick: balanceToPick, | balanceToPick: balanceToPick, | ||||
| pickedQty: line.pickedQty, | |||||
| // 确保 availableQty 不为 null | // 确保 availableQty 不为 null | ||||
| availableQty: availableQty, | availableQty: availableQty, | ||||
| }; | }; | ||||
| @@ -675,6 +677,10 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| try { | try { | ||||
| // ✅ Store current lot selection before refresh | |||||
| const currentSelectedLotRowId = selectedLotRowId; | |||||
| const currentSelectedLotId = selectedLotId; | |||||
| const stockOutLineData: CreateStockOutLine = { | const stockOutLineData: CreateStockOutLine = { | ||||
| consoCode: pickOrderDetails.consoCode, | consoCode: pickOrderDetails.consoCode, | ||||
| pickOrderLineId: selectedRowId, | pickOrderLineId: selectedRowId, | ||||
| @@ -692,19 +698,57 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| if (result) { | if (result) { | ||||
| console.log("Stock out line created successfully:", result); | console.log("Stock out line created successfully:", result); | ||||
| //alert(`Stock out line created successfully! ID: ${result.id}`); | |||||
| // ✅ Don't refresh immediately - let user see the result first | |||||
| // ✅ Auto-refresh data after successful creation | |||||
| console.log("🔄 Refreshing data after stock out line creation..."); | |||||
| try { | |||||
| // ✅ Refresh lot data for the selected row (maintains selection) | |||||
| if (selectedRowId) { | |||||
| await handleRowSelect(selectedRowId, true); // ✅ Preserve lot selection | |||||
| } | |||||
| // ✅ Refresh main pick order details | |||||
| await handleFetchAllPickOrderDetails(); | |||||
| console.log("✅ Data refresh completed - lot selection maintained!"); | |||||
| } catch (refreshError) { | |||||
| console.error("❌ Error refreshing data:", refreshError); | |||||
| } | |||||
| setShowInputBody(false); // Hide preview after successful creation | setShowInputBody(false); // Hide preview after successful creation | ||||
| } else { | } else { | ||||
| console.error("Failed to create stock out line: No response"); | console.error("Failed to create stock out line: No response"); | ||||
| //alert("Failed to create stock out line: No response"); | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error creating stock out line:", error); | console.error("Error creating stock out line:", error); | ||||
| //alert("Error creating stock out line. Please try again."); | |||||
| } | } | ||||
| }, [selectedRowId, pickOrderDetails?.consoCode]); | |||||
| }, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]); | |||||
| // ✅ New function to refresh data while preserving lot selection | |||||
| const handleRefreshDataPreserveSelection = useCallback(async () => { | |||||
| if (!selectedRowId) return; | |||||
| // ✅ Store current lot selection | |||||
| const currentSelectedLotRowId = selectedLotRowId; | |||||
| const currentSelectedLotId = selectedLotId; | |||||
| try { | |||||
| // ✅ Refresh lot data | |||||
| await handleRowSelect(selectedRowId, true); // ✅ Preserve selection | |||||
| // ✅ Refresh main pick order details | |||||
| await handleFetchAllPickOrderDetails(); | |||||
| // ✅ Restore lot selection | |||||
| setSelectedLotRowId(currentSelectedLotRowId); | |||||
| setSelectedLotId(currentSelectedLotId); | |||||
| console.log("✅ Data refreshed with selection preserved"); | |||||
| } catch (error) { | |||||
| console.error("❌ Error refreshing data:", error); | |||||
| } | |||||
| }, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]); | |||||
| // 自定义主表格组件 | // 自定义主表格组件 | ||||
| const CustomMainTable = () => { | const CustomMainTable = () => { | ||||
| @@ -740,7 +784,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const availableQty = line.availableQty ?? 0; | const availableQty = line.availableQty ?? 0; | ||||
| const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 | const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数 | ||||
| const totalPickedQty = getTotalPickedQty(line.id); | const totalPickedQty = getTotalPickedQty(line.id); | ||||
| const actualPickedQty = line.pickedQty ?? 0; | |||||
| return ( | return ( | ||||
| <TableRow | <TableRow | ||||
| key={line.id} | key={line.id} | ||||
| @@ -777,7 +821,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }}> | }}> | ||||
| {availableQty.toLocaleString()} {/* 添加千位分隔符 */} | {availableQty.toLocaleString()} {/* 添加千位分隔符 */} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right">{totalPickedQty}</TableCell> | |||||
| <TableCell align="right">{actualPickedQty}</TableCell> | |||||
| <TableCell align="right">{line.uomDesc}</TableCell> | <TableCell align="right">{line.uomDesc}</TableCell> | ||||
| <TableCell align="right">{line.targetDate}</TableCell> | <TableCell align="right">{line.targetDate}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -40,6 +40,8 @@ interface ExtendedQcItem extends QcItemWithChecks { | |||||
| qcPassed?: boolean; | qcPassed?: boolean; | ||||
| failQty?: number; | failQty?: number; | ||||
| remarks?: string; | remarks?: string; | ||||
| order?: number; // ✅ Add order property | |||||
| stableId?: string; // ✅ Also add stableId for better row identification | |||||
| } | } | ||||
| const style = { | const style = { | ||||
| @@ -195,15 +197,22 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| // ✅ 修改:在组件开始时自动设置失败数量 | // ✅ 修改:在组件开始时自动设置失败数量 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (itemDetail && qcItems.length > 0) { | |||||
| // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||||
| const updatedQcItems = qcItems.map(item => ({ | |||||
| ...item, | |||||
| failQty: itemDetail.requiredQty || 0 // 使用 Lot Required Pick Qty | |||||
| })); | |||||
| setQcItems(updatedQcItems); | |||||
| if (itemDetail && qcItems.length > 0 && selectedLotId) { | |||||
| // ✅ 获取选中的批次数据 | |||||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
| if (selectedLot) { | |||||
| // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||||
| const updatedQcItems = qcItems.map((item, index) => ({ | |||||
| ...item, | |||||
| failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty | |||||
| // ✅ Add stable order and ID fields | |||||
| order: index, | |||||
| stableId: `qc-${item.id}-${index}` | |||||
| })); | |||||
| setQcItems(updatedQcItems); | |||||
| } | |||||
| } | } | ||||
| }, [itemDetail, qcItems.length]); | |||||
| }, [itemDetail, qcItems.length, selectedLotId, lotData]); | |||||
| // ✅ 修改:移除 alert 弹窗,改为控制台日志 | // ✅ 修改:移除 alert 弹窗,改为控制台日志 | ||||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | const onSubmitQc = useCallback<SubmitHandler<any>>( | ||||
| @@ -215,7 +224,8 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| const acceptQty = Number(accQty) || null; | const acceptQty = Number(accQty) || null; | ||||
| const validationErrors : string[] = []; | const validationErrors : string[] = []; | ||||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
| const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | ||||
| if (itemsWithoutResult.length > 0) { | if (itemsWithoutResult.length > 0) { | ||||
| validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | ||||
| @@ -234,7 +244,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| qcItem: item.code, | qcItem: item.code, | ||||
| qcDescription: item.description || "", | qcDescription: item.description || "", | ||||
| isPassed: item.qcPassed, | isPassed: item.qcPassed, | ||||
| failQty: item.qcPassed ? 0 : (itemDetail?.requiredQty || 0), | |||||
| failQty: item.qcPassed ? 0 : (selectedLot?.requiredQty || 0), | |||||
| remarks: item.remarks || "", | remarks: item.remarks || "", | ||||
| })), | })), | ||||
| }; | }; | ||||
| @@ -248,7 +258,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| } | } | ||||
| // ✅ Fix: Update stock out line status based on QC decision | // ✅ Fix: Update stock out line status based on QC decision | ||||
| if (selectedLotId && qcData.qcAccept) { | |||||
| if (selectedLotId) { // ✅ Remove qcData.qcAccept condition | |||||
| try { | try { | ||||
| const allPassed = qcData.qcItems.every(item => item.isPassed); | const allPassed = qcData.qcItems.every(item => item.isPassed); | ||||
| @@ -261,17 +271,19 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| }); | }); | ||||
| // ✅ Fix: 1. Update stock out line status with required qty field | // ✅ Fix: 1. Update stock out line status with required qty field | ||||
| await updateStockOutLineStatus({ | |||||
| id: selectedLotId, | |||||
| status: newStockOutLineStatus, | |||||
| qty: itemDetail?.requiredQty || 0 // ✅ Add required qty field | |||||
| }); | |||||
| if (selectedLot) { | |||||
| await updateStockOutLineStatus({ | |||||
| id: selectedLotId, | |||||
| status: newStockOutLineStatus, | |||||
| qty: selectedLot.requiredQty || 0 | |||||
| }); | |||||
| } else { | |||||
| console.warn("Selected lot not found for stock out line status update"); | |||||
| } | |||||
| // ✅ Fix: 2. If QC failed, also update inventory lot line status | // ✅ Fix: 2. If QC failed, also update inventory lot line status | ||||
| if (!allPassed) { | if (!allPassed) { | ||||
| try { | try { | ||||
| // ✅ Fix: Get the correct lot data | |||||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
| if (selectedLot) { | if (selectedLot) { | ||||
| console.log("Updating inventory lot line status for failed QC:", { | console.log("Updating inventory lot line status for failed QC:", { | ||||
| inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
| @@ -280,7 +292,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| await updateInventoryLotLineStatus({ | await updateInventoryLotLineStatus({ | ||||
| inventoryLotLineId: selectedLot.lotId, | inventoryLotLineId: selectedLot.lotId, | ||||
| status: 'unavailable' // ✅ Use correct backend enum value | |||||
| status: 'unavailable' | |||||
| }); | }); | ||||
| console.log("Inventory lot line status updated to unavailable"); | console.log("Inventory lot line status updated to unavailable"); | ||||
| } else { | } else { | ||||
| @@ -288,7 +300,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to update inventory lot line status:", error); | console.error("Failed to update inventory lot line status:", error); | ||||
| // ✅ Don't fail the entire operation, just log the error | |||||
| } | } | ||||
| } | } | ||||
| @@ -300,12 +311,6 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error updating stock out line status after QC:", error); | console.error("Error updating stock out line status after QC:", error); | ||||
| // ✅ Log detailed error information | |||||
| if (error instanceof Error) { | |||||
| console.error("Error details:", error.message); | |||||
| console.error("Error stack:", error.stack); | |||||
| } | |||||
| // ✅ Don't fail the entire QC submission, just log the error | |||||
| } | } | ||||
| } | } | ||||
| @@ -362,15 +367,20 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | ||||
| onChange={(e) => { | onChange={(e) => { | ||||
| const value = e.target.value === "true"; | const value = e.target.value === "true"; | ||||
| setQcItems((prev) => | |||||
| prev.map((r): ExtendedQcItem => (r.id === params.id ? { ...r, qcPassed: value } : r)) | |||||
| // ✅ Simple state update | |||||
| setQcItems(prev => | |||||
| prev.map(item => | |||||
| item.id === params.id | |||||
| ? { ...item, qcPassed: value } | |||||
| : item | |||||
| ) | |||||
| ); | ); | ||||
| }} | }} | ||||
| name={`qcPassed-${params.id}`} | name={`qcPassed-${params.id}`} | ||||
| > | > | ||||
| <FormControlLabel | <FormControlLabel | ||||
| value="true" | value="true" | ||||
| control={<Radio size="small" />} | |||||
| control={<Radio />} | |||||
| label="合格" | label="合格" | ||||
| sx={{ | sx={{ | ||||
| color: current.qcPassed === true ? "green" : "inherit", | color: current.qcPassed === true ? "green" : "inherit", | ||||
| @@ -379,7 +389,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| /> | /> | ||||
| <FormControlLabel | <FormControlLabel | ||||
| value="false" | value="false" | ||||
| control={<Radio size="small" />} | |||||
| control={<Radio />} | |||||
| label="不合格" | label="不合格" | ||||
| sx={{ | sx={{ | ||||
| color: current.qcPassed === false ? "red" : "inherit", | color: current.qcPassed === false ? "red" : "inherit", | ||||
| @@ -400,7 +410,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| type="number" | type="number" | ||||
| size="small" | size="small" | ||||
| // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | ||||
| value={!params.row.qcPassed ? (itemDetail?.requiredQty || 0) : 0} | |||||
| value={!params.row.qcPassed ? (0) : 0} | |||||
| disabled={params.row.qcPassed} | disabled={params.row.qcPassed} | ||||
| // ✅ 移除 onChange,因为数量是固定的 | // ✅ 移除 onChange,因为数量是固定的 | ||||
| // onChange={(e) => { | // onChange={(e) => { | ||||
| @@ -444,6 +454,27 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| [t], | [t], | ||||
| ); | ); | ||||
| // ✅ Add stable update function | |||||
| const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => { | |||||
| setQcItems(prevItems => | |||||
| prevItems.map(item => | |||||
| item.id === itemId | |||||
| ? { ...item, qcPassed } | |||||
| : item | |||||
| ) | |||||
| ); | |||||
| }, []); | |||||
| // ✅ Remove duplicate functions | |||||
| const getRowId = useCallback((row: any) => { | |||||
| return row.id; // Just use the original ID | |||||
| }, []); | |||||
| // ✅ Remove complex sorting logic | |||||
| // const stableQcItems = useMemo(() => { ... }); // Remove | |||||
| // const sortedQcItems = useMemo(() => { ... }); // Remove | |||||
| // ✅ Use qcItems directly in DataGrid | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| @@ -481,8 +512,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| <StyledDataGrid | <StyledDataGrid | ||||
| columns={qcColumns} | columns={qcColumns} | ||||
| rows={qcItems} | |||||
| rows={qcItems} // ✅ Use qcItems directly | |||||
| autoHeight | autoHeight | ||||
| getRowId={getRowId} // ✅ Simple row ID function | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| </> | </> | ||||
| @@ -0,0 +1,511 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Button, | |||||
| CircularProgress, | |||||
| FormControl, | |||||
| Grid, | |||||
| Modal, | |||||
| TextField, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TablePagination, | |||||
| } from "@mui/material"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| newassignPickOrder, | |||||
| AssignPickOrderInputs, | |||||
| releaseAssignedPickOrders, | |||||
| fetchPickOrderWithStockClient, // Add this import | |||||
| } from "@/app/api/pickOrder/actions"; | |||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||||
| import { | |||||
| FormProvider, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { isEmpty, upperFirst, groupBy } from "lodash"; | |||||
| import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; | |||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||||
| import dayjs from "dayjs"; | |||||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | |||||
| import { sortBy, uniqBy } from "lodash"; | |||||
| import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; | |||||
| dayjs.extend(arraySupport); | |||||
| interface Props { | |||||
| filterArgs: Record<string, any>; | |||||
| } | |||||
| // Update the interface to match the new API response structure | |||||
| interface PickOrderRow { | |||||
| id: string; | |||||
| code: string; | |||||
| targetDate: string; | |||||
| type: string; | |||||
| status: string; | |||||
| assignTo: number; | |||||
| groupName: string; | |||||
| consoCode?: string; | |||||
| pickOrderLines: PickOrderLineRow[]; | |||||
| } | |||||
| interface PickOrderLineRow { | |||||
| id: number; | |||||
| itemId: number; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| availableQty: number | null; | |||||
| requiredQty: number; | |||||
| uomCode: string; | |||||
| uomDesc: string; | |||||
| suggestedList: any[]; | |||||
| } | |||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: { xs: "100%", sm: "100%", md: "100%" }, | |||||
| }; | |||||
| const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| const { setIsUploading } = useUploadContext(); | |||||
| const [isUploading, setIsUploadingLocal] = useState(false); | |||||
| // Update state to use pick order data directly | |||||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); | |||||
| const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]); | |||||
| const [isLoadingItems, setIsLoadingItems] = useState(false); | |||||
| const [pagingController, setPagingController] = useState({ | |||||
| pageNum: 1, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const [totalCountItems, setTotalCountItems] = useState<number>(); | |||||
| const [modalOpen, setModalOpen] = useState(false); | |||||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
| const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]); | |||||
| const formProps = useForm<AssignPickOrderInputs>(); | |||||
| const errors = formProps.formState.errors; | |||||
| // Update the handler functions to work with string IDs | |||||
| const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { | |||||
| if (checked) { | |||||
| setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | |||||
| } else { | |||||
| setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); | |||||
| } | |||||
| }, []); | |||||
| const isPickOrderSelected = useCallback((pickOrderId: string) => { | |||||
| return selectedPickOrderIds.includes(pickOrderId); | |||||
| }, [selectedPickOrderIds]); | |||||
| // Update the fetch function to use the correct endpoint | |||||
| const fetchNewPageItems = useCallback( | |||||
| async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | |||||
| setIsLoadingItems(true); | |||||
| try { | |||||
| const params = { | |||||
| ...pagingController, | |||||
| ...filterArgs, | |||||
| pageNum: (pagingController.pageNum || 1) - 1, | |||||
| pageSize: pagingController.pageSize || 10, | |||||
| // Filter for assigned status only | |||||
| status: "assigned" | |||||
| }; | |||||
| const res = await fetchPickOrderWithStockClient(params); | |||||
| if (res && res.records) { | |||||
| // Convert pick order data to the expected format | |||||
| const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ | |||||
| id: pickOrder.id, | |||||
| code: pickOrder.code, | |||||
| targetDate: pickOrder.targetDate, | |||||
| type: pickOrder.type, | |||||
| status: pickOrder.status, | |||||
| assignTo: pickOrder.assignTo, | |||||
| groupName: pickOrder.groupName || "No Group", | |||||
| consoCode: pickOrder.consoCode, | |||||
| pickOrderLines: pickOrder.pickOrderLines || [] | |||||
| })); | |||||
| setOriginalPickOrderData(pickOrderRows); | |||||
| setFilteredPickOrders(pickOrderRows); | |||||
| setTotalCountItems(res.total); | |||||
| } else { | |||||
| setFilteredPickOrders([]); | |||||
| setTotalCountItems(0); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error fetching pick orders:", error); | |||||
| setFilteredPickOrders([]); | |||||
| setTotalCountItems(0); | |||||
| } finally { | |||||
| setIsLoadingItems(false); | |||||
| } | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| // Handle Release operation | |||||
| // Handle Release operation | |||||
| const handleRelease = useCallback(async () => { | |||||
| if (selectedPickOrderIds.length === 0) return; | |||||
| setIsUploading(true); | |||||
| try { | |||||
| // Get the assigned user from the selected pick orders | |||||
| const selectedPickOrders = filteredPickOrders.filter(pickOrder => | |||||
| selectedPickOrderIds.includes(pickOrder.id) | |||||
| ); | |||||
| // Check if all selected pick orders have the same assigned user | |||||
| const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); | |||||
| if (assignedUsers.length === 0) { | |||||
| alert("Selected pick orders are not assigned to any user."); | |||||
| return; | |||||
| } | |||||
| const assignToValue = assignedUsers[0]; | |||||
| // Validate that all pick orders are assigned to the same user | |||||
| const allSameUser = assignedUsers.every(userId => userId === assignToValue); | |||||
| if (!allSameUser) { | |||||
| alert("All selected pick orders must be assigned to the same user."); | |||||
| return; | |||||
| } | |||||
| console.log("Using assigned user:", assignToValue); | |||||
| console.log("selectedPickOrderIds:", selectedPickOrderIds); | |||||
| const releaseRes = await releaseAssignedPickOrders({ | |||||
| pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), | |||||
| assignTo: assignToValue | |||||
| }); | |||||
| if (releaseRes.code === "SUCCESS") { | |||||
| console.log("Pick orders released successfully"); | |||||
| // Get the consoCode from the response | |||||
| const consoCode = (releaseRes.entity as any)?.consoCode; | |||||
| if (consoCode) { | |||||
| // Create StockOutLine records for each pick order line | |||||
| for (const pickOrder of selectedPickOrders) { | |||||
| for (const line of pickOrder.pickOrderLines) { | |||||
| try { | |||||
| const stockOutLineData = { | |||||
| consoCode: consoCode, | |||||
| pickOrderLineId: line.id, | |||||
| inventoryLotLineId: 0, // This will be set when user scans QR code | |||||
| qty: line.requiredQty, | |||||
| }; | |||||
| console.log("Creating stock out line:", stockOutLineData); | |||||
| await createStockOutLine(stockOutLineData); | |||||
| } catch (error) { | |||||
| console.error("Error creating stock out line for line", line.id, error); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| fetchNewPageItems(pagingController, filterArgs); | |||||
| } else { | |||||
| console.error("Release failed:", releaseRes.message); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error releasing pick orders:", error); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| } | |||||
| }, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
| // Update search criteria to match the new data structure | |||||
| const searchCriteria: Criterion<any>[] = useMemo( | |||||
| () => [ | |||||
| { | |||||
| label: t("Pick Order Code"), | |||||
| paramName: "code", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Group Code"), | |||||
| paramName: "groupName", | |||||
| type: "text", | |||||
| }, | |||||
| { | |||||
| label: t("Target Date From"), | |||||
| label2: t("Target Date To"), | |||||
| paramName: "targetDate", | |||||
| type: "dateRange", | |||||
| }, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| // Update search function to work with pick order data | |||||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||||
| setSearchQuery({ ...query }); | |||||
| const filtered = originalPickOrderData.filter((pickOrder) => { | |||||
| const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); | |||||
| const codeMatch = !query.code || | |||||
| pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||||
| const groupNameMatch = !query.groupName || | |||||
| pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||||
| // Date range search | |||||
| let dateMatch = true; | |||||
| if (query.targetDate || query.targetDateTo) { | |||||
| try { | |||||
| if (query.targetDate && !query.targetDateTo) { | |||||
| const fromDate = dayjs(query.targetDate); | |||||
| dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day'); | |||||
| } else if (!query.targetDate && query.targetDateTo) { | |||||
| const toDate = dayjs(query.targetDateTo); | |||||
| dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day'); | |||||
| } else if (query.targetDate && query.targetDateTo) { | |||||
| const fromDate = dayjs(query.targetDate); | |||||
| const toDate = dayjs(query.targetDateTo); | |||||
| dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day')) && | |||||
| (pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day')); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Date parsing error:", error); | |||||
| dateMatch = true; | |||||
| } | |||||
| } | |||||
| return codeMatch && groupNameMatch && dateMatch; | |||||
| }); | |||||
| setFilteredPickOrders(filtered); | |||||
| }, [originalPickOrderData]); | |||||
| const handleReset = useCallback(() => { | |||||
| setSearchQuery({}); | |||||
| setFilteredPickOrders(originalPickOrderData); | |||||
| setTimeout(() => { | |||||
| setSearchQuery({}); | |||||
| }, 0); | |||||
| }, [originalPickOrderData]); | |||||
| // Pagination handlers | |||||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||||
| const newPagingController = { | |||||
| ...pagingController, | |||||
| pageNum: newPage + 1, | |||||
| }; | |||||
| setPagingController(newPagingController); | |||||
| }, [pagingController]); | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const newPageSize = parseInt(event.target.value, 10); | |||||
| const newPagingController = { | |||||
| pageNum: 1, | |||||
| pageSize: newPageSize, | |||||
| }; | |||||
| setPagingController(newPagingController); | |||||
| }, []); | |||||
| // Component mount effect | |||||
| useEffect(() => { | |||||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||||
| }, []); | |||||
| // Dependencies change effect | |||||
| useEffect(() => { | |||||
| if (pagingController && (filterArgs || {})) { | |||||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||||
| } | |||||
| }, [pagingController, filterArgs, fetchNewPageItems]); | |||||
| useEffect(() => { | |||||
| const loadUsernameList = async () => { | |||||
| try { | |||||
| const res = await fetchNameList(); | |||||
| if (res) { | |||||
| setUsernameList(res); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error loading username list:", error); | |||||
| } | |||||
| }; | |||||
| loadUsernameList(); | |||||
| }, []); | |||||
| // Update the table component to work with pick order data directly | |||||
| const CustomPickOrderTable = () => { | |||||
| // Helper function to get user name | |||||
| const getUserName = useCallback((assignToId: number | null | undefined) => { | |||||
| if (!assignToId) return '-'; | |||||
| const user = usernameList.find(u => u.id === assignToId); | |||||
| return user ? user.name : `User ${assignToId}`; | |||||
| }, [usernameList]); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Selected")}</TableCell> | |||||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||||
| <TableCell>{t("Group Code")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | |||||
| <TableCell>{t("Item Name")}</TableCell> | |||||
| <TableCell align="right">{t("Order Quantity")}</TableCell> | |||||
| <TableCell align="right">{t("Current Stock")}</TableCell> | |||||
| <TableCell align="right">{t("Stock Unit")}</TableCell> | |||||
| <TableCell>{t("Target Date")}</TableCell> | |||||
| <TableCell>{t("Assigned To")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {filteredPickOrders.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={10} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| filteredPickOrders.map((pickOrder) => ( | |||||
| pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( | |||||
| <TableRow key={`${pickOrder.id}-${line.id}`}> | |||||
| {/* Checkbox - only show for first line of each pick order */} | |||||
| <TableCell> | |||||
| {index === 0 ? ( | |||||
| <Checkbox | |||||
| checked={isPickOrderSelected(pickOrder.id)} | |||||
| onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} | |||||
| disabled={!isEmpty(pickOrder.consoCode)} | |||||
| /> | |||||
| ) : null} | |||||
| </TableCell> | |||||
| {/* Pick Order Code - only show for first line */} | |||||
| <TableCell> | |||||
| {index === 0 ? pickOrder.code : null} | |||||
| </TableCell> | |||||
| {/* Group Name - only show for first line */} | |||||
| <TableCell> | |||||
| {index === 0 ? pickOrder.groupName : null} | |||||
| </TableCell> | |||||
| {/* Item Code */} | |||||
| <TableCell>{line.itemCode}</TableCell> | |||||
| {/* Item Name */} | |||||
| <TableCell>{line.itemName}</TableCell> | |||||
| {/* Order Quantity */} | |||||
| <TableCell align="right">{line.requiredQty}</TableCell> | |||||
| {/* Current Stock */} | |||||
| <TableCell align="right"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} | |||||
| > | |||||
| {(line.availableQty || 0).toLocaleString()} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| {/* Unit */} | |||||
| <TableCell align="right">{line.uomDesc}</TableCell> | |||||
| {/* Target Date - only show for first line */} | |||||
| <TableCell> | |||||
| {index === 0 ? ( | |||||
| arrayToDayjs(pickOrder.targetDate) | |||||
| .add(-1, "month") | |||||
| .format(OUTPUT_DATE_FORMAT) | |||||
| ) : null} | |||||
| </TableCell> | |||||
| {/* Assigned To - only show for first line */} | |||||
| <TableCell> | |||||
| {index === 0 ? ( | |||||
| <Typography variant="body2"> | |||||
| {getUserName(pickOrder.assignTo)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={totalCountItems || 0} | |||||
| page={(pagingController.pageNum - 1)} | |||||
| rowsPerPage={pagingController.pageSize} | |||||
| onPageChange={handlePageChange} | |||||
| onRowsPerPageChange={handlePageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50, 100]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| return ( | |||||
| <> | |||||
| <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} /> | |||||
| <Grid container rowGap={1}> | |||||
| <Grid item xs={12}> | |||||
| {isLoadingItems ? ( | |||||
| <CircularProgress size={40} /> | |||||
| ) : ( | |||||
| <CustomPickOrderTable /> | |||||
| )} | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}> | |||||
| <Button | |||||
| disabled={selectedPickOrderIds.length < 1} | |||||
| variant="outlined" | |||||
| onClick={handleRelease} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| </Box> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default AssignTo; | |||||