| @@ -77,11 +77,15 @@ export interface AllPickedStockTakeListReponse { | |||||
| stockTakeSession: string; | stockTakeSession: string; | ||||
| lastStockTakeDate: string | null; | lastStockTakeDate: string | null; | ||||
| status: string|null; | status: string|null; | ||||
| approverName: string | null; | |||||
| currentStockTakeItemNumber: number; | currentStockTakeItemNumber: number; | ||||
| totalInventoryLotNumber: number; | totalInventoryLotNumber: number; | ||||
| stockTakeId: number; | stockTakeId: number; | ||||
| stockTakerName: string | null; | stockTakerName: string | null; | ||||
| totalItemNumber: number; | totalItemNumber: number; | ||||
| startTime: string | null; | |||||
| endTime: string | null; | |||||
| reStockTakeTrueFalse: boolean; | |||||
| } | } | ||||
| export const importStockTake = async (data: FormData) => { | export const importStockTake = async (data: FormData) => { | ||||
| @@ -552,7 +552,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||||
| try { | try { | ||||
| const userIdToUse = userId || currentUserId; | const userIdToUse = userId || currentUserId; | ||||
| console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse); | |||||
| console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); | |||||
| if (!userIdToUse) { | if (!userIdToUse) { | ||||
| console.warn("⚠️ No userId available, skipping API call"); | console.warn("⚠️ No userId available, skipping API call"); | ||||
| @@ -620,9 +620,9 @@ const fgOrder: FGPickOrderResponse = { | |||||
| }; | }; | ||||
| setFgPickOrders([fgOrder]); | setFgPickOrders([fgOrder]); | ||||
| console.log("🔍 DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); | |||||
| console.log("🔍 DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); | |||||
| console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder); | |||||
| console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes); | |||||
| console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| // 移除:不需要 doPickOrderDetail 和 switcher 逻辑 | // 移除:不需要 doPickOrderDetail 和 switcher 逻辑 | ||||
| // if (hierarchicalData.pickOrders.length > 1) { ... } | // if (hierarchicalData.pickOrders.length > 1) { ... } | ||||
| @@ -749,7 +749,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| }); | }); | ||||
| console.log(" Transformed flat lot data:", flatLotData); | console.log(" Transformed flat lot data:", flatLotData); | ||||
| console.log("🔍 Total items (including null stock):", flatLotData.length); | |||||
| console.log(" Total items (including null stock):", flatLotData.length); | |||||
| setCombinedLotData(flatLotData); | setCombinedLotData(flatLotData); | ||||
| setOriginalCombinedData(flatLotData); | setOriginalCombinedData(flatLotData); | ||||
| @@ -766,7 +766,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖 | }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖 | ||||
| // Add effect to check completion when lot data changes | // Add effect to check completion when lot data changes | ||||
| const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => { | const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => { | ||||
| console.log(`🔍 Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`); | |||||
| console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`); | |||||
| // 使用第一个输入框的 lot number 查找当前数据 | // 使用第一个输入框的 lot number 查找当前数据 | ||||
| const currentLot = combinedLotData.find(lot => | const currentLot = combinedLotData.find(lot => | ||||
| @@ -1387,7 +1387,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| return; | return; | ||||
| } | } | ||||
| console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||||
| console.log(` Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||||
| setSelectedLotForQr(expectedLot); | setSelectedLotForQr(expectedLot); | ||||
| handleLotMismatch( | handleLotMismatch( | ||||
| { | { | ||||
| @@ -1424,7 +1424,7 @@ useEffect(() => { | |||||
| return; | return; | ||||
| } | } | ||||
| if (latestQr === "{2fic}") { | if (latestQr === "{2fic}") { | ||||
| console.log("🔍 Detected {2fic} shortcut - opening manual lot confirmation form"); | |||||
| console.log(" Detected {2fic} shortcut - opening manual lot confirmation form"); | |||||
| setManualLotConfirmationOpen(true); | setManualLotConfirmationOpen(true); | ||||
| resetScan(); | resetScan(); | ||||
| setLastProcessedQr(latestQr); | setLastProcessedQr(latestQr); | ||||
| @@ -1432,7 +1432,7 @@ useEffect(() => { | |||||
| return; // 直接返回,不继续处理其他逻辑 | return; // 直接返回,不继续处理其他逻辑 | ||||
| } | } | ||||
| if (latestQr && latestQr !== lastProcessedQr) { | if (latestQr && latestQr !== lastProcessedQr) { | ||||
| console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); | |||||
| console.log(` Processing new QR code with enhanced validation: ${latestQr}`); | |||||
| setLastProcessedQr(latestQr); | setLastProcessedQr(latestQr); | ||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | ||||
| @@ -1921,7 +1921,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| setPickOrderSwitching(true); | setPickOrderSwitching(true); | ||||
| try { | try { | ||||
| console.log("🔍 Switching to pick order:", pickOrderId); | |||||
| console.log(" Switching to pick order:", pickOrderId); | |||||
| setSelectedPickOrderId(pickOrderId); | setSelectedPickOrderId(pickOrderId); | ||||
| // 强制刷新数据,确保显示正确的 pick order 数据 | // 强制刷新数据,确保显示正确的 pick order 数据 | ||||
| @@ -106,6 +106,11 @@ const NavigationContent: React.FC = () => { | |||||
| label: "Finished Good Order", | label: "Finished Good Order", | ||||
| path: "/finishedGood", | path: "/finishedGood", | ||||
| }, | }, | ||||
| { | |||||
| icon: <RequestQuote />, | |||||
| label: "Stock Record", | |||||
| path: "/stockRecord", | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| // { | // { | ||||
| @@ -362,8 +362,8 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| // FIXED: 计算累计拣货数量 | // FIXED: 计算累计拣货数量 | ||||
| const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | ||||
| console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0); | console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0); | ||||
| console.log("🔍 DEBUG - Current submit:", qty); | |||||
| console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot); | |||||
| console.log(" DEBUG - Current submit:", qty); | |||||
| console.log(" DEBUG - Total picked:", totalPickedForThisLot); | |||||
| console.log("�� DEBUG - Required qty:", selectedLot.requiredQty); | console.log("�� DEBUG - Required qty:", selectedLot.requiredQty); | ||||
| // FIXED: 状态应该基于累计拣货数量 | // FIXED: 状态应该基于累计拣货数量 | ||||
| @@ -428,7 +428,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| if (currentConsoCode) { | if (currentConsoCode) { | ||||
| try { | try { | ||||
| console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`); | |||||
| console.log(` Checking completion for consoCode: ${currentConsoCode}`); | |||||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode); | const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode); | ||||
| console.log("�� Completion response:", completionResponse); | console.log("�� Completion response:", completionResponse); | ||||
| @@ -788,7 +788,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||||
| const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); | const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); | ||||
| if (foundLine) { | if (foundLine) { | ||||
| correctConsoCode = pickOrder.consoCode; | correctConsoCode = pickOrder.consoCode; | ||||
| console.log(`🔍 Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`); | |||||
| console.log(` Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`); | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| @@ -58,7 +58,66 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||||
| const startIdx = page * PER_PAGE; | const startIdx = page * PER_PAGE; | ||||
| const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); | ||||
| const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => { | |||||
| const [currentTime, setCurrentTime] = useState(dayjs()); | |||||
| useEffect(() => { | |||||
| if (!endTime && startTime) { | |||||
| const interval = setInterval(() => { | |||||
| setCurrentTime(dayjs()); | |||||
| }, 1000); // 每秒更新一次 | |||||
| return () => clearInterval(interval); | |||||
| } | |||||
| }, [startTime, endTime]); | |||||
| if (endTime && startTime) { | |||||
| // 当有结束时间时,计算从开始到结束的持续时间 | |||||
| const start = dayjs(startTime); | |||||
| const end = dayjs(endTime); | |||||
| const duration = dayjs.duration(end.diff(start)); | |||||
| const hours = Math.floor(duration.asHours()); | |||||
| const minutes = duration.minutes(); | |||||
| const seconds = duration.seconds(); | |||||
| return ( | |||||
| <> | |||||
| {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')} | |||||
| </> | |||||
| ); | |||||
| } else if (startTime) { | |||||
| // 当没有结束时间时,显示实时计时器 | |||||
| const start = dayjs(startTime); | |||||
| const duration = dayjs.duration(currentTime.diff(start)); | |||||
| const hours = Math.floor(duration.asHours()); | |||||
| const minutes = duration.minutes(); | |||||
| const seconds = duration.seconds(); | |||||
| return ( | |||||
| <> | |||||
| {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')} | |||||
| </> | |||||
| ); | |||||
| } else { | |||||
| return <>-</>; | |||||
| } | |||||
| }; | |||||
| const startTimeDisplay = (startTime: string | null) => { | |||||
| if (startTime) { | |||||
| const start = dayjs(startTime); | |||||
| return start.format("HH:mm"); | |||||
| } else { | |||||
| return "-"; | |||||
| } | |||||
| }; | |||||
| const endTimeDisplay = (endTime: string | null) => { | |||||
| if (endTime) { | |||||
| const end = dayjs(endTime); | |||||
| return end.format("HH:mm"); | |||||
| } else { | |||||
| return "-"; | |||||
| } | |||||
| }; | |||||
| const getStatusColor = (status: string | null) => { | const getStatusColor = (status: string | null) => { | ||||
| if (!status) return "default"; | if (!status) return "default"; | ||||
| @@ -126,17 +185,22 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||||
| <Typography variant="subtitle1" fontWeight={600}> | <Typography variant="subtitle1" fontWeight={600}> | ||||
| {t("Section")}: {session.stockTakeSession} | {t("Section")}: {session.stockTakeSession} | ||||
| </Typography> | </Typography> | ||||
| {session.status ? ( | |||||
| <Chip size="small" label={t(session.status)} color={statusColor as any} /> | |||||
| ) : ( | |||||
| <Chip size="small" label={t(" ")} color="default" /> | |||||
| )} | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| </Typography> | |||||
| </Stack> | </Stack> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName || "-"}</Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Approver")}: {session.approverName || "-"}</Typography> | |||||
| </Stack> | |||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | ||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} /> | |||||
| </Typography> | </Typography> | ||||
| {session.totalInventoryLotNumber > 0 && ( | {session.totalInventoryLotNumber > 0 && ( | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}> | <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}> | ||||
| @@ -156,7 +220,7 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||||
| )} | )} | ||||
| </CardContent> | </CardContent> | ||||
| <CardActions sx={{ pt: 0.5 }}> | |||||
| <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| @@ -169,7 +233,13 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||||
| }} | }} | ||||
| > | > | ||||
| {t("View Details")} | {t("View Details")} | ||||
| </Button> | </Button> | ||||
| {session.status ? ( | |||||
| <Chip size="small" label={t(session.status)} color={statusColor as any} /> | |||||
| ) : ( | |||||
| <Chip size="small" label={t(" ")} color="default" /> | |||||
| )} | |||||
| </CardActions> | </CardActions> | ||||
| </Card> | </Card> | ||||
| </Grid> | </Grid> | ||||
| @@ -252,7 +252,20 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| handleBatchSubmitAllRef.current = handleBatchSubmitAll; | handleBatchSubmitAllRef.current = handleBatchSubmitAll; | ||||
| }, [handleBatchSubmitAll]); | }, [handleBatchSubmitAll]); | ||||
| const formatNumber = (num: number | null | undefined): string => { | |||||
| if (num == null) return "0.00"; | |||||
| return num.toLocaleString('en-US', { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2 | |||||
| }); | |||||
| }; | |||||
| const uniqueWarehouses = Array.from( | |||||
| new Set( | |||||
| inventoryLotDetails | |||||
| .map(detail => detail.warehouse) | |||||
| .filter(warehouse => warehouse && warehouse.trim() !== "") | |||||
| ) | |||||
| ).join(", "); | |||||
| const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { | ||||
| // Only allow editing if there's a first stock take qty | // Only allow editing if there's a first stock take qty | ||||
| if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { | if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { | ||||
| @@ -266,9 +279,18 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | ||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||||
| <Typography variant="h6" sx={{ mb: 2 }}> | <Typography variant="h6" sx={{ mb: 2 }}> | ||||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | {t("Stock Take Section")}: {selectedSession.stockTakeSession} | ||||
| {uniqueWarehouses && ( | |||||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| <Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}> | |||||
| {t("Batch Save All")} | |||||
| </Button> | |||||
| </Stack> | |||||
| {loadingDetails ? ( | {loadingDetails ? ( | ||||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | ||||
| <CircularProgress /> | <CircularProgress /> | ||||
| @@ -279,8 +301,8 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Warehouse Location")}</TableCell> | <TableCell>{t("Warehouse Location")}</TableCell> | ||||
| <TableCell>{t("Item")}</TableCell> | |||||
| <TableCell>{t("Stock Take Qty")}</TableCell> | |||||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("UOM")}</TableCell> | <TableCell>{t("UOM")}</TableCell> | ||||
| <TableCell>{t("Record Status")}</TableCell> | <TableCell>{t("Record Status")}</TableCell> | ||||
| @@ -316,21 +338,21 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box> | <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box> | ||||
| <Box>{detail.lotNo || "-"}</Box> | <Box>{detail.lotNo || "-"}</Box> | ||||
| <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box> | <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box> | ||||
| <Box><Chip size="small" label={t(detail.status)} color="default" /></Box> | |||||
| {/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/} | |||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell sx={{ minWidth: 300 }}> | <TableCell sx={{ minWidth: 300 }}> | ||||
| {detail.finalQty != null ? ( | {detail.finalQty != null ? ( | ||||
| // 提交后只显示差异行 | |||||
| <Stack spacing={0.5}> | <Stack spacing={0.5}> | ||||
| <Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}> | <Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}> | ||||
| {t("Difference")}: {detail.finalQty?.toFixed(2) || "0.00"} - {(detail.availableQty || 0).toFixed(2)} = {((detail.finalQty || 0) - (detail.availableQty || 0)).toFixed(2)} | |||||
| {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber((detail.finalQty || 0) - (detail.availableQty || 0))} | |||||
| </Typography> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| ) : ( | ) : ( | ||||
| <Stack spacing={1}> | <Stack spacing={1}> | ||||
| {/* 第一行:First Qty(默认选中) */} | |||||
| {hasFirst && ( | {hasFirst && ( | ||||
| <Stack direction="row" spacing={1} alignItems="center"> | <Stack direction="row" spacing={1} alignItems="center"> | ||||
| <Radio | <Radio | ||||
| @@ -339,12 +361,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })} | onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })} | ||||
| /> | /> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {t("First")}: {(detail.firstStockTakeQty??0)+(detail.firstBadQty??0) || "0.00"} ({detail.firstBadQty??0}) | |||||
| {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} | |||||
| </Typography> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| )} | )} | ||||
| {/* 第二行:Second Qty(如果存在) */} | |||||
| {hasSecond && ( | {hasSecond && ( | ||||
| <Stack direction="row" spacing={1} alignItems="center"> | <Stack direction="row" spacing={1} alignItems="center"> | ||||
| <Radio | <Radio | ||||
| @@ -353,12 +375,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })} | onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })} | ||||
| /> | /> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {t("Second")}: {(detail.secondStockTakeQty??0)+(detail.secondBadQty??0) || "0.00"} ({detail.secondBadQty??0}) | |||||
| {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} | |||||
| </Typography> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| )} | )} | ||||
| {/* 第三行:Approver Input(仅在 second qty 存在时显示) */} | |||||
| {hasSecond && ( | {hasSecond && ( | ||||
| <Stack direction="row" spacing={1} alignItems="center"> | <Stack direction="row" spacing={1} alignItems="center"> | ||||
| <Radio | <Radio | ||||
| @@ -372,22 +394,42 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| type="number" | type="number" | ||||
| value={approverQty[detail.id] || ""} | value={approverQty[detail.id] || ""} | ||||
| onChange={(e) => setApproverQty({ ...approverQty, [detail.id]: e.target.value })} | onChange={(e) => setApproverQty({ ...approverQty, [detail.id]: e.target.value })} | ||||
| sx={{ width: 100 }} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| '& .MuiInputBase-input': { | |||||
| height: '1.4375em', | |||||
| padding: '4px 8px' | |||||
| } | |||||
| }} | |||||
| placeholder={t("Stock Take Qty") } | |||||
| disabled={selection !== "approver"} | disabled={selection !== "approver"} | ||||
| /> | /> | ||||
| <Typography variant="body2">-</Typography> | |||||
| <TextField | <TextField | ||||
| size="small" | size="small" | ||||
| type="number" | type="number" | ||||
| value={approverBadQty[detail.id] || ""} | value={approverBadQty[detail.id] || ""} | ||||
| onChange={(e) => setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} | onChange={(e) => setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} | ||||
| sx={{ width: 100 }} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| '& .MuiInputBase-input': { | |||||
| height: '1.4375em', | |||||
| padding: '4px 8px' | |||||
| } | |||||
| }} | |||||
| placeholder={t("Bad Qty")} | |||||
| disabled={selection !== "approver"} | disabled={selection !== "approver"} | ||||
| /> | /> | ||||
| <Typography variant="body2"> | |||||
| ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} | |||||
| </Typography> | |||||
| </Stack> | </Stack> | ||||
| )} | )} | ||||
| {/* 差异行:显示 selected qty - bookqty = result */} | |||||
| {(() => { | {(() => { | ||||
| let selectedQty = 0; | let selectedQty = 0; | ||||
| @@ -396,7 +438,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| } else if (selection === "second") { | } else if (selection === "second") { | ||||
| selectedQty = detail.secondStockTakeQty || 0; | selectedQty = detail.secondStockTakeQty || 0; | ||||
| } else if (selection === "approver") { | } else if (selection === "approver") { | ||||
| selectedQty = parseFloat(approverQty[detail.id] || "0") || 0; | |||||
| selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; | |||||
| } | } | ||||
| const bookQty = detail.availableQty || 0; | const bookQty = detail.availableQty || 0; | ||||
| @@ -404,7 +446,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| return ( | return ( | ||||
| <Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}> | <Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}> | ||||
| {t("Difference")}: {selectedQty.toFixed(2)} - {bookQty.toFixed(2)} = {difference.toFixed(2)} | |||||
| {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} | |||||
| </Typography> | </Typography> | ||||
| ); | ); | ||||
| })()} | })()} | ||||
| @@ -431,6 +473,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( | {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( | ||||
| <Box> | |||||
| <Button | <Button | ||||
| size="small" | size="small" | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -440,8 +483,11 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| > | > | ||||
| {t("ReStockTake")} | {t("ReStockTake")} | ||||
| </Button> | </Button> | ||||
| </Box> | |||||
| )} | )} | ||||
| <br/> | |||||
| {detail.finalQty == null && ( | {detail.finalQty == null && ( | ||||
| <Box> | |||||
| <Button | <Button | ||||
| size="small" | size="small" | ||||
| variant="contained" | variant="contained" | ||||
| @@ -450,6 +496,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||||
| > | > | ||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| </Box> | |||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -16,6 +16,7 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useState, useCallback, useEffect } from "react"; | import { useState, useCallback, useEffect } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import duration from "dayjs/plugin/duration"; | |||||
| import { | import { | ||||
| getStockTakeRecords, | getStockTakeRecords, | ||||
| AllPickedStockTakeListReponse, | AllPickedStockTakeListReponse, | ||||
| @@ -33,7 +34,9 @@ interface PickerCardListProps { | |||||
| const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => { | const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => { | ||||
| const { t } = useTranslation(["inventory", "common"]); | const { t } = useTranslation(["inventory", "common"]); | ||||
| dayjs.extend(duration); | |||||
| const PER_PAGE = 6; | |||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); | const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); | ||||
| const [page, setPage] = useState(0); | const [page, setPage] = useState(0); | ||||
| @@ -88,10 +91,70 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| if (statusLower === "completed") return "success"; | if (statusLower === "completed") return "success"; | ||||
| if (statusLower === "in_progress" || statusLower === "processing") return "primary"; | if (statusLower === "in_progress" || statusLower === "processing") return "primary"; | ||||
| if (statusLower === "approving") return "info"; | if (statusLower === "approving") return "info"; | ||||
| if (statusLower === "stockTaking") return "primary"; | |||||
| if (statusLower === "no_cycle") return "default"; | if (statusLower === "no_cycle") return "default"; | ||||
| return "warning"; | return "warning"; | ||||
| }; | }; | ||||
| const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => { | |||||
| const [currentTime, setCurrentTime] = useState(dayjs()); | |||||
| useEffect(() => { | |||||
| if (!endTime && startTime) { | |||||
| const interval = setInterval(() => { | |||||
| setCurrentTime(dayjs()); | |||||
| }, 1000); // 每秒更新一次 | |||||
| return () => clearInterval(interval); | |||||
| } | |||||
| }, [startTime, endTime]); | |||||
| if (endTime && startTime) { | |||||
| // 当有结束时间时,计算从开始到结束的持续时间 | |||||
| const start = dayjs(startTime); | |||||
| const end = dayjs(endTime); | |||||
| const duration = dayjs.duration(end.diff(start)); | |||||
| const hours = Math.floor(duration.asHours()); | |||||
| const minutes = duration.minutes(); | |||||
| const seconds = duration.seconds(); | |||||
| return ( | |||||
| <> | |||||
| {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')} | |||||
| </> | |||||
| ); | |||||
| } else if (startTime) { | |||||
| // 当没有结束时间时,显示实时计时器 | |||||
| const start = dayjs(startTime); | |||||
| const duration = dayjs.duration(currentTime.diff(start)); | |||||
| const hours = Math.floor(duration.asHours()); | |||||
| const minutes = duration.minutes(); | |||||
| const seconds = duration.seconds(); | |||||
| return ( | |||||
| <> | |||||
| {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')} | |||||
| </> | |||||
| ); | |||||
| } else { | |||||
| return <>-</>; | |||||
| } | |||||
| }; | |||||
| const startTimeDisplay = (startTime: string | null) => { | |||||
| if (startTime) { | |||||
| const start = dayjs(startTime); | |||||
| return start.format("HH:mm"); | |||||
| } else { | |||||
| return "-"; | |||||
| } | |||||
| }; | |||||
| const endTimeDisplay = (endTime: string | null) => { | |||||
| if (endTime) { | |||||
| const end = dayjs(endTime); | |||||
| return end.format("HH:mm"); | |||||
| } else { | |||||
| return "-"; | |||||
| } | |||||
| }; | |||||
| const getCompletionRate = (session: AllPickedStockTakeListReponse): number => { | const getCompletionRate = (session: AllPickedStockTakeListReponse): number => { | ||||
| if (session.totalInventoryLotNumber === 0) return 0; | if (session.totalInventoryLotNumber === 0) return 0; | ||||
| return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); | return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); | ||||
| @@ -145,13 +208,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| <Typography variant="subtitle1" fontWeight={600}> | <Typography variant="subtitle1" fontWeight={600}> | ||||
| {t("Section")}: {session.stockTakeSession} | {t("Section")}: {session.stockTakeSession} | ||||
| </Typography> | </Typography> | ||||
| <Chip size="small" label={t(session.status || "")} color={statusColor as any} /> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| </Typography> | |||||
| </Stack> | </Stack> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography> | |||||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography> | |||||
| </Stack> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | ||||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||||
| {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} /> | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography> | <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography> | ||||
| {session.totalInventoryLotNumber > 0 && ( | {session.totalInventoryLotNumber > 0 && ( | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| @@ -172,7 +243,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| )} | )} | ||||
| </CardContent> | </CardContent> | ||||
| <CardActions sx={{ pt: 0.5 }}> | |||||
| <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| @@ -184,9 +256,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||||
| variant="contained" | variant="contained" | ||||
| size="small" | size="small" | ||||
| onClick={() => onReStockTakeClick(session)} | onClick={() => onReStockTakeClick(session)} | ||||
| disabled={!session.reStockTakeTrueFalse} | |||||
| > | > | ||||
| {t("View ReStockTake")} | {t("View ReStockTake")} | ||||
| </Button> | </Button> | ||||
| </Stack> | |||||
| <Chip size="small" label={t(session.status || "")} color={statusColor as any} /> | |||||
| </CardActions> | </CardActions> | ||||
| </Card> | </Card> | ||||
| </Grid> | </Grid> | ||||
| @@ -292,14 +292,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| } | } | ||||
| return false; | return false; | ||||
| }, []); | }, []); | ||||
| const uniqueWarehouses = Array.from( | |||||
| new Set( | |||||
| inventoryLotDetails | |||||
| .map(detail => detail.warehouse) | |||||
| .filter(warehouse => warehouse && warehouse.trim() !== "") | |||||
| ) | |||||
| ).join(", "); | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | ||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| <Typography variant="h6" sx={{ mb: 2 }}> | <Typography variant="h6" sx={{ mb: 2 }}> | ||||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||||
| {uniqueWarehouses && ( | |||||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| {/* | {/* | ||||
| {shortcutInput && ( | {shortcutInput && ( | ||||
| @@ -320,17 +330,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Warehouse Location")}</TableCell> | <TableCell>{t("Warehouse Location")}</TableCell> | ||||
| <TableCell>{t("Item")}</TableCell> | |||||
| {/*<TableCell>{t("Item Name")}</TableCell>*/} | |||||
| {/*<TableCell>{t("Lot No")}</TableCell>*/} | |||||
| <TableCell>{t("Expiry Date")}</TableCell> | |||||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||||
| <TableCell>{t("Qty")}</TableCell> | <TableCell>{t("Qty")}</TableCell> | ||||
| <TableCell>{t("Bad Qty")}</TableCell> | <TableCell>{t("Bad Qty")}</TableCell> | ||||
| {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} | {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} | ||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("UOM")}</TableCell> | <TableCell>{t("UOM")}</TableCell> | ||||
| <TableCell>{t("Status")}</TableCell> | |||||
| <TableCell>{t("Record Status")}</TableCell> | <TableCell>{t("Record Status")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | <TableCell>{t("Action")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -353,29 +361,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| return ( | return ( | ||||
| <TableRow key={detail.id}> | <TableRow key={detail.id}> | ||||
| <TableCell>{detail.warehouseCode || "-"}</TableCell> | |||||
| <TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell> | |||||
| <TableCell sx={{ | <TableCell sx={{ | ||||
| maxWidth: 100, | |||||
| maxWidth: 150, | |||||
| wordBreak: 'break-word', | wordBreak: 'break-word', | ||||
| whiteSpace: 'normal', | whiteSpace: 'normal', | ||||
| lineHeight: 1.5 | lineHeight: 1.5 | ||||
| }}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell> | |||||
| {/* | |||||
| <TableCell | |||||
| sx={{ | |||||
| maxWidth: 200, | |||||
| wordBreak: 'break-word', | |||||
| whiteSpace: 'normal', | |||||
| lineHeight: 1.5 | |||||
| }} | |||||
| > | |||||
| {detail.itemName || "-"} | |||||
| </TableCell>*/} | |||||
| {/*<TableCell>{detail.lotNo || "-"}</TableCell>*/} | |||||
| <TableCell> | |||||
| {detail.expiryDate | |||||
| ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) | |||||
| : "-"} | |||||
| }}> | |||||
| <Stack spacing={0.5}> | |||||
| <Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box> | |||||
| <Box>{detail.lotNo || "-"}</Box> | |||||
| <Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box> | |||||
| {/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/} | |||||
| </Stack> | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| @@ -418,73 +416,69 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| <Stack spacing={0.5}> | |||||
| {isEditing && isFirstSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstBadQty} | |||||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.firstBadQty ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("First")}: {detail.firstBadQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondBadQty} | |||||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.secondBadQty ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("Second")}: {detail.secondBadQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| - | |||||
| </Typography> | |||||
| )} | |||||
| </Stack> | |||||
| </TableCell> | |||||
| <TableCell sx={{ width: 180 }}> | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <> | |||||
| <Typography variant="body2">{t("Remark")}</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| value={remark} | |||||
| onChange={(e) => setRemark(e.target.value)} | |||||
| sx={{ width: 150 }} | |||||
| // If you want a single-line input, remove multiline/rows: | |||||
| // multiline | |||||
| // rows={2} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <Typography variant="body2"> | |||||
| {detail.remarks || "-"} | |||||
| </Typography> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell>{detail.uom || "-"}</TableCell> | |||||
| <TableCell> | |||||
| {detail.status ? ( | |||||
| <Chip size="small" label={t(detail.status)} color="default" /> | |||||
| <Stack spacing={0.5}> | |||||
| {isEditing && isFirstSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstBadQty} | |||||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("First")}: {detail.firstBadQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : ( | ) : ( | ||||
| "-" | |||||
| <Typography variant="body2" sx={{ visibility: 'hidden' }}> | |||||
| {t("First")}: 0.00 | |||||
| </Typography> | |||||
| )} | )} | ||||
| </TableCell> | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondBadQty} | |||||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("Second")}: {detail.secondBadQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| - | |||||
| </Typography> | |||||
| )} | |||||
| </Stack> | |||||
| </TableCell> | |||||
| <TableCell sx={{ width: 180 }}> | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <> | |||||
| <Typography variant="body2">{t("Remark")}</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| value={remark} | |||||
| onChange={(e) => setRemark(e.target.value)} | |||||
| sx={{ width: 150 }} | |||||
| // If you want a single-line input, remove multiline/rows: | |||||
| // multiline | |||||
| // rows={2} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <Typography variant="body2"> | |||||
| {detail.remarks || "-"} | |||||
| </Typography> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell>{detail.uom || "-"}</TableCell> | |||||
| <TableCell> | <TableCell> | ||||
| {detail.stockTakeRecordStatus === "pass" ? ( | {detail.stockTakeRecordStatus === "pass" ? ( | ||||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | ||||
| @@ -18,8 +18,8 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useState, useCallback, useEffect, useRef } from "react"; | import { useState, useCallback, useEffect, useRef } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | |||||
| AllPickedStockTakeListReponse, | |||||
| import { | |||||
| AllPickedStockTakeListReponse, | |||||
| getInventoryLotDetailsBySection, | getInventoryLotDetailsBySection, | ||||
| InventoryLotDetailResponse, | InventoryLotDetailResponse, | ||||
| saveStockTakeRecord, | saveStockTakeRecord, | ||||
| @@ -51,6 +51,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| // 编辑状态 | // 编辑状态 | ||||
| const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null); | const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null); | ||||
| // firstQty / secondQty 保存的是 total = available + bad | |||||
| const [firstQty, setFirstQty] = useState<string>(""); | const [firstQty, setFirstQty] = useState<string>(""); | ||||
| const [secondQty, setSecondQty] = useState<string>(""); | const [secondQty, setSecondQty] = useState<string>(""); | ||||
| const [firstBadQty, setFirstBadQty] = useState<string>(""); | const [firstBadQty, setFirstBadQty] = useState<string>(""); | ||||
| @@ -84,8 +85,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { | const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { | ||||
| setEditingRecord(detail); | setEditingRecord(detail); | ||||
| setFirstQty(detail.firstStockTakeQty?.toString() || ""); | |||||
| setSecondQty(detail.secondStockTakeQty?.toString() || ""); | |||||
| // 编辑时,输入 total = qty + badQty | |||||
| const firstTotal = | |||||
| detail.firstStockTakeQty != null | |||||
| ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString() | |||||
| : ""; | |||||
| const secondTotal = | |||||
| detail.secondStockTakeQty != null | |||||
| ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString() | |||||
| : ""; | |||||
| setFirstQty(firstTotal); | |||||
| setSecondQty(secondTotal); | |||||
| setFirstBadQty(detail.firstBadQty?.toString() || ""); | setFirstBadQty(detail.firstBadQty?.toString() || ""); | ||||
| setSecondBadQty(detail.secondBadQty?.toString() || ""); | setSecondBadQty(detail.secondBadQty?.toString() || ""); | ||||
| setRemark(detail.remarks || ""); | setRemark(detail.remarks || ""); | ||||
| @@ -100,125 +112,164 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| setRemark(""); | setRemark(""); | ||||
| }, []); | }, []); | ||||
| const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { | |||||
| if (!selectedSession || !currentUserId) { | |||||
| return; | |||||
| } | |||||
| const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||||
| const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||||
| const qty = isFirstSubmit ? firstQty : secondQty; | |||||
| const badQty = isFirstSubmit ? firstBadQty : secondBadQty; | |||||
| if (!qty || !badQty) { | |||||
| onSnackbar( | |||||
| isFirstSubmit | |||||
| ? t("Please enter QTY and Bad QTY") | |||||
| : t("Please enter Second QTY and Bad QTY"), | |||||
| "error" | |||||
| ); | |||||
| return; | |||||
| } | |||||
| setSaving(true); | |||||
| try { | |||||
| const request: SaveStockTakeRecordRequest = { | |||||
| stockTakeRecordId: detail.stockTakeRecordId || null, | |||||
| inventoryLotLineId: detail.id, | |||||
| qty: parseFloat(qty), | |||||
| badQty: parseFloat(badQty), | |||||
| remark: isSecondSubmit ? (remark || null) : null, | |||||
| }; | |||||
| console.log('handleSaveStockTake: request:', request); | |||||
| console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); | |||||
| console.log('handleSaveStockTake: currentUserId:', currentUserId); | |||||
| await saveStockTakeRecord( | |||||
| request, | |||||
| selectedSession.stockTakeId, | |||||
| currentUserId | |||||
| ); | |||||
| onSnackbar(t("Stock take record saved successfully"), "success"); | |||||
| handleCancelEdit(); | |||||
| const details = await getInventoryLotDetailsBySection( | |||||
| selectedSession.stockTakeSession, | |||||
| selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null | |||||
| ); | |||||
| setInventoryLotDetails(Array.isArray(details) ? details : []); | |||||
| } catch (e: any) { | |||||
| console.error("Save stock take record error:", e); | |||||
| let errorMessage = t("Failed to save stock take record"); | |||||
| if (e?.message) { | |||||
| errorMessage = e.message; | |||||
| } else if (e?.response) { | |||||
| try { | |||||
| const errorData = await e.response.json(); | |||||
| errorMessage = errorData.message || errorData.error || errorMessage; | |||||
| } catch { | |||||
| // ignore | |||||
| const formatNumber = (num: number | null | undefined): string => { | |||||
| if (num == null || Number.isNaN(num)) return "0.00"; | |||||
| return num.toLocaleString("en-US", { | |||||
| minimumFractionDigits: 2, | |||||
| maximumFractionDigits: 2, | |||||
| }); | |||||
| }; | |||||
| const handleSaveStockTake = useCallback( | |||||
| async (detail: InventoryLotDetailResponse) => { | |||||
| if (!selectedSession || !currentUserId) { | |||||
| return; | |||||
| } | |||||
| const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||||
| const isSecondSubmit = | |||||
| detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||||
| // 现在用户输入的是 total 和 bad,需要算 available = total - bad | |||||
| const totalQtyStr = isFirstSubmit ? firstQty : secondQty; | |||||
| const badQtyStr = isFirstSubmit ? firstBadQty : secondBadQty; | |||||
| if (!totalQtyStr || !badQtyStr) { | |||||
| onSnackbar( | |||||
| isFirstSubmit | |||||
| ? t("Please enter QTY and Bad QTY") | |||||
| : t("Please enter Second QTY and Bad QTY"), | |||||
| "error" | |||||
| ); | |||||
| return; | |||||
| } | |||||
| const totalQty = parseFloat(totalQtyStr); | |||||
| const badQty = parseFloat(badQtyStr); | |||||
| if (Number.isNaN(totalQty) || Number.isNaN(badQty)) { | |||||
| onSnackbar(t("Invalid QTY or Bad QTY"), "error"); | |||||
| return; | |||||
| } | |||||
| const availableQty = totalQty - badQty; | |||||
| if (availableQty < 0) { | |||||
| onSnackbar(t("Available QTY cannot be negative"), "error"); | |||||
| return; | |||||
| } | |||||
| setSaving(true); | |||||
| try { | |||||
| const request: SaveStockTakeRecordRequest = { | |||||
| stockTakeRecordId: detail.stockTakeRecordId || null, | |||||
| inventoryLotLineId: detail.id, | |||||
| qty: availableQty, // 保存 available qty | |||||
| badQty: badQty, // 保存 bad qty | |||||
| remark: isSecondSubmit ? (remark || null) : null, | |||||
| }; | |||||
| console.log("handleSaveStockTake: request:", request); | |||||
| console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId); | |||||
| console.log("handleSaveStockTake: currentUserId:", currentUserId); | |||||
| await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId); | |||||
| onSnackbar(t("Stock take record saved successfully"), "success"); | |||||
| handleCancelEdit(); | |||||
| const details = await getInventoryLotDetailsBySection( | |||||
| selectedSession.stockTakeSession, | |||||
| selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null | |||||
| ); | |||||
| setInventoryLotDetails(Array.isArray(details) ? details : []); | |||||
| } catch (e: any) { | |||||
| console.error("Save stock take record error:", e); | |||||
| let errorMessage = t("Failed to save stock take record"); | |||||
| if (e?.message) { | |||||
| errorMessage = e.message; | |||||
| } else if (e?.response) { | |||||
| try { | |||||
| const errorData = await e.response.json(); | |||||
| errorMessage = errorData.message || errorData.error || errorMessage; | |||||
| } catch { | |||||
| // ignore | |||||
| } | |||||
| } | } | ||||
| onSnackbar(errorMessage, "error"); | |||||
| } finally { | |||||
| setSaving(false); | |||||
| } | } | ||||
| onSnackbar(errorMessage, "error"); | |||||
| } finally { | |||||
| setSaving(false); | |||||
| } | |||||
| }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]); | |||||
| }, | |||||
| [ | |||||
| selectedSession, | |||||
| firstQty, | |||||
| secondQty, | |||||
| firstBadQty, | |||||
| secondBadQty, | |||||
| remark, | |||||
| handleCancelEdit, | |||||
| t, | |||||
| currentUserId, | |||||
| onSnackbar, | |||||
| ] | |||||
| ); | |||||
| const handleBatchSubmitAll = useCallback(async () => { | |||||
| if (!selectedSession || !currentUserId) { | |||||
| console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); | |||||
| return; | |||||
| } | |||||
| const handleBatchSubmitAll = useCallback( | |||||
| async () => { | |||||
| if (!selectedSession || !currentUserId) { | |||||
| console.log("handleBatchSubmitAll: Missing selectedSession or currentUserId"); | |||||
| return; | |||||
| } | |||||
| console.log("handleBatchSubmitAll: Starting batch save..."); | |||||
| setBatchSaving(true); | |||||
| try { | |||||
| const request: BatchSaveStockTakeRecordRequest = { | |||||
| stockTakeId: selectedSession.stockTakeId, | |||||
| stockTakeSection: selectedSession.stockTakeSession, | |||||
| stockTakerId: currentUserId, | |||||
| }; | |||||
| const result = await batchSaveStockTakeRecords(request); | |||||
| console.log("handleBatchSubmitAll: Result:", result); | |||||
| onSnackbar( | |||||
| t("Batch save completed: {{success}} success, {{errors}} errors", { | |||||
| success: result.successCount, | |||||
| errors: result.errorCount, | |||||
| }), | |||||
| result.errorCount > 0 ? "warning" : "success" | |||||
| ); | |||||
| console.log('handleBatchSubmitAll: Starting batch save...'); | |||||
| setBatchSaving(true); | |||||
| try { | |||||
| const request: BatchSaveStockTakeRecordRequest = { | |||||
| stockTakeId: selectedSession.stockTakeId, | |||||
| stockTakeSection: selectedSession.stockTakeSession, | |||||
| stockTakerId: currentUserId, | |||||
| }; | |||||
| const result = await batchSaveStockTakeRecords(request); | |||||
| console.log('handleBatchSubmitAll: Result:', result); | |||||
| onSnackbar( | |||||
| t("Batch save completed: {{success}} success, {{errors}} errors", { | |||||
| success: result.successCount, | |||||
| errors: result.errorCount, | |||||
| }), | |||||
| result.errorCount > 0 ? "warning" : "success" | |||||
| ); | |||||
| const details = await getInventoryLotDetailsBySection( | |||||
| selectedSession.stockTakeSession, | |||||
| selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null | |||||
| ); | |||||
| setInventoryLotDetails(Array.isArray(details) ? details : []); | |||||
| } catch (e: any) { | |||||
| console.error("handleBatchSubmitAll: Error:", e); | |||||
| let errorMessage = t("Failed to batch save stock take records"); | |||||
| if (e?.message) { | |||||
| errorMessage = e.message; | |||||
| } else if (e?.response) { | |||||
| try { | |||||
| const errorData = await e.response.json(); | |||||
| errorMessage = errorData.message || errorData.error || errorMessage; | |||||
| } catch { | |||||
| // ignore | |||||
| const details = await getInventoryLotDetailsBySection( | |||||
| selectedSession.stockTakeSession, | |||||
| selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null | |||||
| ); | |||||
| setInventoryLotDetails(Array.isArray(details) ? details : []); | |||||
| } catch (e: any) { | |||||
| console.error("handleBatchSubmitAll: Error:", e); | |||||
| let errorMessage = t("Failed to batch save stock take records"); | |||||
| if (e?.message) { | |||||
| errorMessage = e.message; | |||||
| } else if (e?.response) { | |||||
| try { | |||||
| const errorData = await e.response.json(); | |||||
| errorMessage = errorData.message || errorData.error || errorMessage; | |||||
| } catch { | |||||
| // ignore | |||||
| } | |||||
| } | } | ||||
| onSnackbar(errorMessage, "error"); | |||||
| } finally { | |||||
| setBatchSaving(false); | |||||
| } | } | ||||
| onSnackbar(errorMessage, "error"); | |||||
| } finally { | |||||
| setBatchSaving(false); | |||||
| } | |||||
| }, [selectedSession, t, currentUserId, onSnackbar]); | |||||
| }, | |||||
| [selectedSession, t, currentUserId, onSnackbar] | |||||
| ); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| handleBatchSubmitAllRef.current = handleBatchSubmitAll; | handleBatchSubmitAllRef.current = handleBatchSubmitAll; | ||||
| @@ -227,11 +278,12 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const handleKeyPress = (e: KeyboardEvent) => { | const handleKeyPress = (e: KeyboardEvent) => { | ||||
| const target = e.target as HTMLElement; | const target = e.target as HTMLElement; | ||||
| if (target && ( | |||||
| target.tagName === 'INPUT' || | |||||
| target.tagName === 'TEXTAREA' || | |||||
| target.isContentEditable | |||||
| )) { | |||||
| if ( | |||||
| target && | |||||
| (target.tagName === "INPUT" || | |||||
| target.tagName === "TEXTAREA" || | |||||
| target.isContentEditable) | |||||
| ) { | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -240,48 +292,48 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| } | } | ||||
| if (e.key.length === 1) { | if (e.key.length === 1) { | ||||
| setShortcutInput(prev => { | |||||
| setShortcutInput((prev) => { | |||||
| const newInput = prev + e.key; | const newInput = prev + e.key; | ||||
| if (newInput === '{2fitestall}') { | |||||
| console.log('✅ Shortcut {2fitestall} detected!'); | |||||
| if (newInput === "{2fitestall}") { | |||||
| console.log("✅ Shortcut {2fitestall} detected!"); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| if (handleBatchSubmitAllRef.current) { | if (handleBatchSubmitAllRef.current) { | ||||
| console.log('Calling handleBatchSubmitAll...'); | |||||
| handleBatchSubmitAllRef.current().catch(err => { | |||||
| console.error('Error in handleBatchSubmitAll:', err); | |||||
| console.log("Calling handleBatchSubmitAll..."); | |||||
| handleBatchSubmitAllRef.current().catch((err) => { | |||||
| console.error("Error in handleBatchSubmitAll:", err); | |||||
| }); | }); | ||||
| } else { | } else { | ||||
| console.error('handleBatchSubmitAllRef.current is null'); | |||||
| console.error("handleBatchSubmitAllRef.current is null"); | |||||
| } | } | ||||
| }, 0); | }, 0); | ||||
| return ""; | return ""; | ||||
| } | } | ||||
| if (newInput.length > 15) { | if (newInput.length > 15) { | ||||
| return ""; | return ""; | ||||
| } | } | ||||
| if (newInput.length > 0 && !newInput.startsWith('{')) { | |||||
| if (newInput.length > 0 && !newInput.startsWith("{")) { | |||||
| return ""; | return ""; | ||||
| } | } | ||||
| if (newInput.length > 5 && !newInput.startsWith('{2fi')) { | |||||
| if (newInput.length > 5 && !newInput.startsWith("{2fi")) { | |||||
| return ""; | return ""; | ||||
| } | } | ||||
| return newInput; | return newInput; | ||||
| }); | }); | ||||
| } else if (e.key === 'Backspace') { | |||||
| setShortcutInput(prev => prev.slice(0, -1)); | |||||
| } else if (e.key === 'Escape') { | |||||
| } else if (e.key === "Backspace") { | |||||
| setShortcutInput((prev) => prev.slice(0, -1)); | |||||
| } else if (e.key === "Escape") { | |||||
| setShortcutInput(""); | setShortcutInput(""); | ||||
| } | } | ||||
| }; | }; | ||||
| window.addEventListener('keydown', handleKeyPress); | |||||
| window.addEventListener("keydown", handleKeyPress); | |||||
| return () => { | return () => { | ||||
| window.removeEventListener('keydown', handleKeyPress); | |||||
| window.removeEventListener("keydown", handleKeyPress); | |||||
| }; | }; | ||||
| }, []); | }, []); | ||||
| @@ -292,19 +344,46 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| return false; | return false; | ||||
| }, []); | }, []); | ||||
| const uniqueWarehouses = Array.from( | |||||
| new Set( | |||||
| inventoryLotDetails | |||||
| .map((detail) => detail.warehouse) | |||||
| .filter((warehouse) => warehouse && warehouse.trim() !== "") | |||||
| ) | |||||
| ).join(", "); | |||||
| return ( | return ( | ||||
| <Box> | <Box> | ||||
| <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | |||||
| <Button | |||||
| onClick={onBack} | |||||
| sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }} | |||||
| > | |||||
| {t("Back to List")} | {t("Back to List")} | ||||
| </Button> | </Button> | ||||
| <Typography variant="h6" sx={{ mb: 2 }}> | <Typography variant="h6" sx={{ mb: 2 }}> | ||||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | {t("Stock Take Section")}: {selectedSession.stockTakeSession} | ||||
| {uniqueWarehouses && ( | |||||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||||
| )} | |||||
| </Typography> | </Typography> | ||||
| {/* 如果需要显示快捷键输入,可以把这块注释打开 */} | |||||
| {/* | {/* | ||||
| {shortcutInput && ( | {shortcutInput && ( | ||||
| <Box sx={{ mb: 2, p: 1.5, bgcolor: 'info.light', borderRadius: 1, border: '1px solid', borderColor: 'info.main' }}> | |||||
| <Box | |||||
| sx={{ | |||||
| mb: 2, | |||||
| p: 1.5, | |||||
| bgcolor: "info.light", | |||||
| borderRadius: 1, | |||||
| border: "1px solid", | |||||
| borderColor: "info.main", | |||||
| }} | |||||
| > | |||||
| <Typography variant="body2" color="info.dark" fontWeight={500}> | <Typography variant="body2" color="info.dark" fontWeight={500}> | ||||
| {t("Shortcut Input")}: <strong style={{ fontFamily: 'monospace', fontSize: '1.1em' }}>{shortcutInput}</strong> | |||||
| {t("Shortcut Input")}:{" "} | |||||
| <strong style={{ fontFamily: "monospace", fontSize: "1.1em" }}> | |||||
| {shortcutInput} | |||||
| </strong> | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| @@ -319,17 +398,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Warehouse Location")}</TableCell> | <TableCell>{t("Warehouse Location")}</TableCell> | ||||
| <TableCell>{t("Item")}</TableCell> | |||||
| {/*<TableCell>{t("Item Name")}</TableCell>*/} | |||||
| {/*<TableCell>{t("Lot No")}</TableCell>*/} | |||||
| <TableCell>{t("Expiry Date")}</TableCell> | |||||
| <TableCell>{t("Qty")}</TableCell> | |||||
| <TableCell>{t("Bad Qty")}</TableCell> | |||||
| {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} | |||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell> | |||||
| <TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell> | |||||
| <TableCell>{t("Remark")}</TableCell> | |||||
| <TableCell>{t("UOM")}</TableCell> | <TableCell>{t("UOM")}</TableCell> | ||||
| <TableCell>{t("Status")}</TableCell> | |||||
| <TableCell>{t("Record Status")}</TableCell> | <TableCell>{t("Record Status")}</TableCell> | ||||
| <TableCell>{t("Action")}</TableCell> | <TableCell>{t("Action")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -337,7 +409,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| <TableBody> | <TableBody> | ||||
| {inventoryLotDetails.length === 0 ? ( | {inventoryLotDetails.length === 0 ? ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={12} align="center"> | |||||
| <TableCell colSpan={7} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No data")} | {t("No data")} | ||||
| </Typography> | </Typography> | ||||
| @@ -347,152 +419,215 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| inventoryLotDetails.map((detail) => { | inventoryLotDetails.map((detail) => { | ||||
| const isEditing = editingRecord?.id === detail.id; | const isEditing = editingRecord?.id === detail.id; | ||||
| const submitDisabled = isSubmitDisabled(detail); | const submitDisabled = isSubmitDisabled(detail); | ||||
| const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||||
| const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; | |||||
| const isFirstSubmit = | |||||
| !detail.stockTakeRecordId || !detail.firstStockTakeQty; | |||||
| const isSecondSubmit = | |||||
| detail.stockTakeRecordId && | |||||
| detail.firstStockTakeQty && | |||||
| !detail.secondStockTakeQty; | |||||
| return ( | return ( | ||||
| <TableRow key={detail.id}> | <TableRow key={detail.id}> | ||||
| <TableCell>{detail.warehouseCode || "-"}</TableCell> | |||||
| <TableCell sx={{ | |||||
| maxWidth: 100, | |||||
| wordBreak: 'break-word', | |||||
| whiteSpace: 'normal', | |||||
| lineHeight: 1.5 | |||||
| }}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell> | |||||
| {/* | |||||
| <TableCell | |||||
| sx={{ | |||||
| maxWidth: 200, | |||||
| wordBreak: 'break-word', | |||||
| whiteSpace: 'normal', | |||||
| lineHeight: 1.5 | |||||
| }} | |||||
| > | |||||
| {detail.itemName || "-"} | |||||
| </TableCell>*/} | |||||
| {/*<TableCell>{detail.lotNo || "-"}</TableCell>*/} | |||||
| <TableCell> | <TableCell> | ||||
| {detail.expiryDate | |||||
| ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) | |||||
| : "-"} | |||||
| {detail.warehouseArea || "-"} | |||||
| {detail.warehouseSlot || "-"} | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <TableCell | |||||
| sx={{ | |||||
| maxWidth: 150, | |||||
| wordBreak: "break-word", | |||||
| whiteSpace: "normal", | |||||
| lineHeight: 1.5, | |||||
| }} | |||||
| > | |||||
| <Stack spacing={0.5}> | <Stack spacing={0.5}> | ||||
| {isEditing && isFirstSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstQty} | |||||
| onChange={(e) => setFirstQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.firstStockTakeQty ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("First")}: {detail.firstStockTakeQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondQty} | |||||
| onChange={(e) => setSecondQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.secondStockTakeQty ? ( | |||||
| <Typography variant="body2"> | |||||
| {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} | |||||
| </Typography> | |||||
| ) : null} | |||||
| {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| - | |||||
| </Typography> | |||||
| )} | |||||
| <Box> | |||||
| {detail.itemCode || "-"} {detail.itemName || "-"} | |||||
| </Box> | |||||
| <Box>{detail.lotNo || "-"}</Box> | |||||
| <Box> | |||||
| {detail.expiryDate | |||||
| ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) | |||||
| : "-"} | |||||
| </Box> | |||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| <Stack spacing={0.5}> | |||||
| {/* Qty + Bad Qty 合并显示/输入 */} | |||||
| <TableCell sx={{ minWidth: 300 }}> | |||||
| <Stack spacing={1}> | |||||
| {/* First */} | |||||
| {isEditing && isFirstSubmit ? ( | {isEditing && isFirstSubmit ? ( | ||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstBadQty} | |||||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.firstBadQty ? ( | |||||
| <Stack direction="row" spacing={1} alignItems="center"> | |||||
| <Typography variant="body2">{t("First")}:</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstQty} | |||||
| onChange={(e) => setFirstQty(e.target.value)} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| "& .MuiInputBase-input": { | |||||
| height: "1.4375em", | |||||
| padding: "4px 8px", | |||||
| }, | |||||
| }} | |||||
| placeholder={t("Stock Take Qty")} | |||||
| /> | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={firstBadQty} | |||||
| onChange={(e) => setFirstBadQty(e.target.value)} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| "& .MuiInputBase-input": { | |||||
| height: "1.4375em", | |||||
| padding: "4px 8px", | |||||
| }, | |||||
| }} | |||||
| placeholder={t("Bad Qty")} | |||||
| /> | |||||
| <Typography variant="body2"> | |||||
| = | |||||
| {formatNumber( | |||||
| parseFloat(firstQty || "0") - | |||||
| parseFloat(firstBadQty || "0") | |||||
| )} | |||||
| </Typography> | |||||
| </Stack> | |||||
| ) : detail.firstStockTakeQty != null ? ( | |||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {t("First")}: {detail.firstBadQty.toFixed(2)} | |||||
| {t("First")}:{" "} | |||||
| {formatNumber( | |||||
| (detail.firstStockTakeQty ?? 0) + | |||||
| (detail.firstBadQty ?? 0) | |||||
| )}{" "} | |||||
| ( | |||||
| {formatNumber( | |||||
| detail.firstBadQty ?? 0 | |||||
| )} | |||||
| ) ={" "} | |||||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||||
| </Typography> | </Typography> | ||||
| ) : null} | ) : null} | ||||
| {/* Second */} | |||||
| {isEditing && isSecondSubmit ? ( | {isEditing && isSecondSubmit ? ( | ||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondBadQty} | |||||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||||
| sx={{ width: 100 }} | |||||
| /> | |||||
| ) : detail.secondBadQty ? ( | |||||
| <Stack direction="row" spacing={1} alignItems="center"> | |||||
| <Typography variant="body2">{t("Second")}:</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondQty} | |||||
| onChange={(e) => setSecondQty(e.target.value)} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| "& .MuiInputBase-input": { | |||||
| height: "1.4375em", | |||||
| padding: "4px 8px", | |||||
| }, | |||||
| }} | |||||
| placeholder={t("Stock Take Qty")} | |||||
| /> | |||||
| <TextField | |||||
| size="small" | |||||
| type="number" | |||||
| value={secondBadQty} | |||||
| onChange={(e) => setSecondBadQty(e.target.value)} | |||||
| sx={{ | |||||
| width: 130, | |||||
| minWidth: 130, | |||||
| "& .MuiInputBase-input": { | |||||
| height: "1.4375em", | |||||
| padding: "4px 8px", | |||||
| }, | |||||
| }} | |||||
| placeholder={t("Bad Qty")} | |||||
| /> | |||||
| <Typography variant="body2"> | |||||
| = | |||||
| {formatNumber( | |||||
| parseFloat(secondQty || "0") - | |||||
| parseFloat(secondBadQty || "0") | |||||
| )} | |||||
| </Typography> | |||||
| </Stack> | |||||
| ) : detail.secondStockTakeQty != null ? ( | |||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {t("Second")}: {detail.secondBadQty.toFixed(2)} | |||||
| {t("Second")}:{" "} | |||||
| {formatNumber( | |||||
| (detail.secondStockTakeQty ?? 0) + | |||||
| (detail.secondBadQty ?? 0) | |||||
| )}{" "} | |||||
| ( | |||||
| {formatNumber( | |||||
| detail.secondBadQty ?? 0 | |||||
| )} | |||||
| ) ={" "} | |||||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||||
| </Typography> | </Typography> | ||||
| ) : null} | ) : null} | ||||
| {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| - | |||||
| </Typography> | |||||
| )} | |||||
| {!detail.firstStockTakeQty && | |||||
| !detail.secondStockTakeQty && | |||||
| !isEditing && ( | |||||
| <Typography | |||||
| variant="body2" | |||||
| color="text.secondary" | |||||
| > | |||||
| - | |||||
| </Typography> | |||||
| )} | |||||
| </Stack> | </Stack> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Remark */} | |||||
| <TableCell sx={{ width: 180 }}> | <TableCell sx={{ width: 180 }}> | ||||
| {isEditing && isSecondSubmit ? ( | |||||
| <> | |||||
| <Typography variant="body2">{t("Remark")}</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| value={remark} | |||||
| onChange={(e) => setRemark(e.target.value)} | |||||
| sx={{ width: 150 }} | |||||
| // If you want a single-line input, remove multiline/rows: | |||||
| // multiline | |||||
| // rows={2} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <Typography variant="body2"> | |||||
| {detail.remarks || "-"} | |||||
| </Typography> | |||||
| )} | |||||
| </TableCell> | |||||
| <TableCell>{detail.uom || "-"}</TableCell> | |||||
| <TableCell> | |||||
| {detail.status ? ( | |||||
| <Chip size="small" label={t(detail.status)} color="default" /> | |||||
| {isEditing && isSecondSubmit ? ( | |||||
| <> | |||||
| <Typography variant="body2">{t("Remark")}</Typography> | |||||
| <TextField | |||||
| size="small" | |||||
| value={remark} | |||||
| onChange={(e) => setRemark(e.target.value)} | |||||
| sx={{ width: 150 }} | |||||
| /> | |||||
| </> | |||||
| ) : ( | ) : ( | ||||
| "-" | |||||
| <Typography variant="body2"> | |||||
| {detail.remarks || "-"} | |||||
| </Typography> | |||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell>{detail.uom || "-"}</TableCell> | |||||
| <TableCell> | <TableCell> | ||||
| {detail.stockTakeRecordStatus === "pass" ? ( | {detail.stockTakeRecordStatus === "pass" ? ( | ||||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||||
| <Chip | |||||
| size="small" | |||||
| label={t(detail.stockTakeRecordStatus)} | |||||
| color="success" | |||||
| /> | |||||
| ) : detail.stockTakeRecordStatus === "notMatch" ? ( | ) : detail.stockTakeRecordStatus === "notMatch" ? ( | ||||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" /> | |||||
| <Chip | |||||
| size="small" | |||||
| label={t(detail.stockTakeRecordStatus)} | |||||
| color="warning" | |||||
| /> | |||||
| ) : ( | ) : ( | ||||
| <Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" /> | |||||
| <Chip | |||||
| size="small" | |||||
| label={t(detail.stockTakeRecordStatus || "")} | |||||
| color="default" | |||||
| /> | |||||
| )} | )} | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | <TableCell> | ||||
| {isEditing ? ( | {isEditing ? ( | ||||
| <Stack direction="row" spacing={1}> | <Stack direction="row" spacing={1}> | ||||
| @@ -504,13 +639,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| > | > | ||||
| {t("Save")} | {t("Save")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| size="small" | |||||
| onClick={handleCancelEdit} | |||||
| > | |||||
| <Button size="small" onClick={handleCancelEdit}> | |||||
| {t("Cancel")} | {t("Cancel")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| ) : ( | ) : ( | ||||
| <Button | <Button | ||||
| @@ -519,10 +650,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||||
| onClick={() => handleStartEdit(detail)} | onClick={() => handleStartEdit(detail)} | ||||
| disabled={submitDisabled} | disabled={submitDisabled} | ||||
| > | > | ||||
| {!detail.stockTakeRecordId | |||||
| ? t("Input") | |||||
| : detail.stockTakeRecordStatus === "notMatch" | |||||
| ? t("Input") | |||||
| {!detail.stockTakeRecordId | |||||
| ? t("Input") | |||||
| : detail.stockTakeRecordStatus === "notMatch" | |||||
| ? t("Input") | |||||
| : t("View")} | : t("View")} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| @@ -1,13 +1,13 @@ | |||||
| import React from "react"; | import React from "react"; | ||||
| import GeneralLoading from "../General/GeneralLoading"; | import GeneralLoading from "../General/GeneralLoading"; | ||||
| import StockTakeManagement from "./StockTakeManagement"; | import StockTakeManagement from "./StockTakeManagement"; | ||||
| import StockTakeTabs from "./StockTakeTab"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof GeneralLoading; | Loading: typeof GeneralLoading; | ||||
| } | } | ||||
| const StockTakeManagementWrapper: React.FC & SubComponents = async () => { | const StockTakeManagementWrapper: React.FC & SubComponents = async () => { | ||||
| return <StockTakeManagement />; | |||||
| return <StockTakeTabs/>; | |||||
| }; | }; | ||||
| StockTakeManagementWrapper.Loading = GeneralLoading; | StockTakeManagementWrapper.Loading = GeneralLoading; | ||||
| @@ -4,7 +4,7 @@ | |||||
| "Job Order Production Process": "工單生產流程", | "Job Order Production Process": "工單生產流程", | ||||
| "productionProcess": "生產流程", | "productionProcess": "生產流程", | ||||
| "Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
| "All": "全部", | |||||
| "Stock Record": "庫存記錄", | |||||
| "No options": "沒有選項", | "No options": "沒有選項", | ||||
| "Select Another Bag Lot": "選擇另一個包裝袋", | "Select Another Bag Lot": "選擇另一個包裝袋", | ||||
| "Finished QC Job Orders": "完成QC工單", | "Finished QC Job Orders": "完成QC工單", | ||||
| @@ -28,6 +28,13 @@ | |||||
| "Total finished QC job orders": "總完成QC工單數量", | "Total finished QC job orders": "總完成QC工單數量", | ||||
| "Over Time": "超時", | "Over Time": "超時", | ||||
| "Code": "編號", | "Code": "編號", | ||||
| "Job Order No.": "工單編號", | |||||
| "FG / WIP Item": "成品/半成品", | |||||
| "Production Time Remaining": "生產剩餘時間", | |||||
| "Process": "工序", | |||||
| "Start": "開始", | |||||
| "Finish": "完成", | |||||
| "Wait Time [minutes]": "等待時間(分鐘)", | |||||
| "Staff No": "員工編號", | "Staff No": "員工編號", | ||||
| "code": "編號", | "code": "編號", | ||||
| "Name": "名稱", | "Name": "名稱", | ||||
| @@ -41,6 +48,8 @@ | |||||
| "No": "沒有", | "No": "沒有", | ||||
| "Assignment failed: ": "分配失敗: ", | "Assignment failed: ": "分配失敗: ", | ||||
| "Unknown error": "未知錯誤", | "Unknown error": "未知錯誤", | ||||
| "Job Process Status": "工單流程狀態", | |||||
| "FG / WIP Item": "成品/半成品", | |||||
| "WIP": "半成品", | "WIP": "半成品", | ||||
| "R&D": "研發", | "R&D": "研發", | ||||
| "STF": "樣品", | "STF": "樣品", | ||||
| @@ -10,12 +10,29 @@ | |||||
| "fg": "成品", | "fg": "成品", | ||||
| "Back to List": "返回列表", | "Back to List": "返回列表", | ||||
| "Record Status": "記錄狀態", | "Record Status": "記錄狀態", | ||||
| "Stock take record status updated to not match": "盤點記錄狀態更新為數值不符", | |||||
| "available": "可用", | "available": "可用", | ||||
| "Item-lotNo-ExpiryDate": "貨品-批號-到期日", | |||||
| "not available": "不可用", | "not available": "不可用", | ||||
| "Batch Submit All": "批量提交所有", | |||||
| "Batch Save All": "批量保存所有", | |||||
| "not match": "數值不符", | "not match": "數值不符", | ||||
| "Stock Take Qty": "盤點數量(含壞盤點數量)", | |||||
| "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數(含壞品)= 可用數", | |||||
| "View ReStockTake": "查看重新盤點", | "View ReStockTake": "查看重新盤點", | ||||
| "Stock Take Qty": "盤點數", | |||||
| "ReStockTake": "重新盤點", | "ReStockTake": "重新盤點", | ||||
| "Stock Taker": "盤點員", | |||||
| "Total Item Number": "貨品數量", | |||||
| "Start Time": "開始時間", | |||||
| "Difference": "差異", | |||||
| "stockTaking": "盤點中", | |||||
| "selected stock take qty": "已選擇盤點數量", | |||||
| "book qty": "帳面庫存", | |||||
| "start time": "開始時間", | |||||
| "end time": "結束時間", | |||||
| "Only Variance": "僅差異", | |||||
| "Control Time": "操作時間", | |||||
| "pass": "通過", | "pass": "通過", | ||||
| "not pass": "不通過", | "not pass": "不通過", | ||||
| "Available": "可用", | "Available": "可用", | ||||
| @@ -23,7 +40,7 @@ | |||||
| "pending": "待處理", | "pending": "待處理", | ||||
| "Last Stock Take Date": "上次盤點日期", | "Last Stock Take Date": "上次盤點日期", | ||||
| "Remark": "備註", | "Remark": "備註", | ||||
| "notMatch": "不匹配", | |||||
| "notMatch": "數值不符", | |||||
| "Stock take record saved successfully": "盤點記錄保存成功", | "Stock take record saved successfully": "盤點記錄保存成功", | ||||
| "View Details": "查看詳細", | "View Details": "查看詳細", | ||||
| "Input": "輸入", | "Input": "輸入", | ||||
| @@ -133,5 +150,17 @@ | |||||
| "Stock take adjustment confirmed! (Demo only)": "盤點調整確認!(僅演示)", | "Stock take adjustment confirmed! (Demo only)": "盤點調整確認!(僅演示)", | ||||
| "Stock take adjustment has been confirmed successfully!": "盤點調整確認成功!", | "Stock take adjustment has been confirmed successfully!": "盤點調整確認成功!", | ||||
| "System Qty": "系統數量", | "System Qty": "系統數量", | ||||
| "Variance": "差異" | |||||
| "Variance": "差異", | |||||
| "Stock Record": "庫存記錄", | |||||
| "Item-lotNo": "貨品-批號", | |||||
| "In Qty": "入庫數量", | |||||
| "Out Qty": "出庫數量", | |||||
| "Balance Qty": "庫存數量", | |||||
| "Start Date": "開始日期", | |||||
| "End Date": "結束日期", | |||||
| "Loading": "加載中", | |||||
| "adj": "調整", | |||||
| "nor": "正常" | |||||
| } | } | ||||
| @@ -101,6 +101,13 @@ | |||||
| "Job Order Pickexcution": "工單提料", | "Job Order Pickexcution": "工單提料", | ||||
| "Pick Order Detail": "提料單細節", | "Pick Order Detail": "提料單細節", | ||||
| "Finished Job Order Record": "已完成工單記錄", | "Finished Job Order Record": "已完成工單記錄", | ||||
| "No. of Items to be Picked": "需提料數量", | |||||
| "No. of Items with Issue During Pick": "提料過程中出現問題的數量", | |||||
| "Pick Start Time": "提料開始時間", | |||||
| "Pick End Time": "提料結束時間", | |||||
| "FG / WIP Item": "成品/半成品", | |||||
| "Pick Order No.- Job Order No.- Item": "提料單編號-工單編號-成品/半成品", | |||||
| "Pick Time Taken (minutes)": "提料時間(分鐘)", | |||||
| "Index": "編號", | "Index": "編號", | ||||
| "Route": "路線", | "Route": "路線", | ||||
| "Qty": "數量", | "Qty": "數量", | ||||
| @@ -517,6 +524,13 @@ | |||||
| "Start Scan": "開始掃碼", | "Start Scan": "開始掃碼", | ||||
| "Stop Scan": "停止掃碼", | "Stop Scan": "停止掃碼", | ||||
| "Sign out": "登出" | |||||
| "Material Pick Status": "物料提料狀態", | |||||
| "Job Order Qty": "工單數量", | |||||
| "Sign out": "登出", | |||||
| "Job Order No.": "工單編號", | |||||
| "FG / WIP Item": "成品/半成品", | |||||
| "Production Time Remaining": "生產剩餘時間", | |||||
| "Process": "工序", | |||||
| "Start": "開始", | |||||
| "Finish": "完成" | |||||
| } | } | ||||