| @@ -77,11 +77,15 @@ export interface AllPickedStockTakeListReponse { | |||
| stockTakeSession: string; | |||
| lastStockTakeDate: string | null; | |||
| status: string|null; | |||
| approverName: string | null; | |||
| currentStockTakeItemNumber: number; | |||
| totalInventoryLotNumber: number; | |||
| stockTakeId: number; | |||
| stockTakerName: string | null; | |||
| totalItemNumber: number; | |||
| startTime: string | null; | |||
| endTime: string | null; | |||
| reStockTakeTrueFalse: boolean; | |||
| } | |||
| export const importStockTake = async (data: FormData) => { | |||
| @@ -552,7 +552,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| try { | |||
| const userIdToUse = userId || currentUserId; | |||
| console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse); | |||
| console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); | |||
| if (!userIdToUse) { | |||
| console.warn("⚠️ No userId available, skipping API call"); | |||
| @@ -620,9 +620,9 @@ const fgOrder: FGPickOrderResponse = { | |||
| }; | |||
| 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 逻辑 | |||
| // 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("🔍 Total items (including null stock):", flatLotData.length); | |||
| console.log(" Total items (including null stock):", flatLotData.length); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| @@ -766,7 +766,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖 | |||
| // Add effect to check completion when lot data changes | |||
| 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 查找当前数据 | |||
| const currentLot = combinedLotData.find(lot => | |||
| @@ -1387,7 +1387,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return; | |||
| } | |||
| console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||
| console.log(` Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||
| setSelectedLotForQr(expectedLot); | |||
| handleLotMismatch( | |||
| { | |||
| @@ -1424,7 +1424,7 @@ useEffect(() => { | |||
| return; | |||
| } | |||
| 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); | |||
| resetScan(); | |||
| setLastProcessedQr(latestQr); | |||
| @@ -1432,7 +1432,7 @@ useEffect(() => { | |||
| return; // 直接返回,不继续处理其他逻辑 | |||
| } | |||
| 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); | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| @@ -1921,7 +1921,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| setPickOrderSwitching(true); | |||
| try { | |||
| console.log("🔍 Switching to pick order:", pickOrderId); | |||
| console.log(" Switching to pick order:", pickOrderId); | |||
| setSelectedPickOrderId(pickOrderId); | |||
| // 强制刷新数据,确保显示正确的 pick order 数据 | |||
| @@ -106,6 +106,11 @@ const NavigationContent: React.FC = () => { | |||
| label: "Finished Good Order", | |||
| path: "/finishedGood", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Stock Record", | |||
| path: "/stockRecord", | |||
| }, | |||
| ], | |||
| }, | |||
| // { | |||
| @@ -362,8 +362,8 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| // FIXED: 计算累计拣货数量 | |||
| const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty; | |||
| 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); | |||
| // FIXED: 状态应该基于累计拣货数量 | |||
| @@ -428,7 +428,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| if (currentConsoCode) { | |||
| try { | |||
| console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`); | |||
| console.log(` Checking completion for consoCode: ${currentConsoCode}`); | |||
| const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode); | |||
| console.log("�� Completion response:", completionResponse); | |||
| @@ -788,7 +788,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number) | |||
| const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); | |||
| if (foundLine) { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -58,7 +58,66 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||
| const startIdx = page * 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) => { | |||
| if (!status) return "default"; | |||
| @@ -126,17 +185,22 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||
| <Typography variant="subtitle1" fontWeight={600}> | |||
| {t("Section")}: {session.stockTakeSession} | |||
| </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 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 }}> | |||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||
| {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} /> | |||
| </Typography> | |||
| {session.totalInventoryLotNumber > 0 && ( | |||
| <Box sx={{ mt: 2 }}> | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}> | |||
| @@ -156,7 +220,7 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||
| )} | |||
| </CardContent> | |||
| <CardActions sx={{ pt: 0.5 }}> | |||
| <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}> | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| @@ -169,7 +233,13 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => { | |||
| }} | |||
| > | |||
| {t("View Details")} | |||
| </Button> | |||
| {session.status ? ( | |||
| <Chip size="small" label={t(session.status)} color={statusColor as any} /> | |||
| ) : ( | |||
| <Chip size="small" label={t(" ")} color="default" /> | |||
| )} | |||
| </CardActions> | |||
| </Card> | |||
| </Grid> | |||
| @@ -252,7 +252,20 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| useEffect(() => { | |||
| handleBatchSubmitAllRef.current = 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 => { | |||
| // Only allow editing if there's a first stock take qty | |||
| 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" }}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}> | |||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||
| {uniqueWarehouses && ( | |||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||
| )} | |||
| </Typography> | |||
| <Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}> | |||
| {t("Batch Save All")} | |||
| </Button> | |||
| </Stack> | |||
| {loadingDetails ? ( | |||
| <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> | |||
| <CircularProgress /> | |||
| @@ -279,8 +301,8 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| <TableHead> | |||
| <TableRow> | |||
| <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("UOM")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| @@ -316,21 +338,21 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| <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> | |||
| {/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/} | |||
| </Stack> | |||
| </TableCell> | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| {detail.finalQty != null ? ( | |||
| // 提交后只显示差异行 | |||
| <Stack spacing={0.5}> | |||
| <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> | |||
| </Stack> | |||
| ) : ( | |||
| <Stack spacing={1}> | |||
| {/* 第一行:First Qty(默认选中) */} | |||
| {hasFirst && ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Radio | |||
| @@ -339,12 +361,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })} | |||
| /> | |||
| <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> | |||
| </Stack> | |||
| )} | |||
| {/* 第二行:Second Qty(如果存在) */} | |||
| {hasSecond && ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Radio | |||
| @@ -353,12 +375,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })} | |||
| /> | |||
| <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> | |||
| </Stack> | |||
| )} | |||
| {/* 第三行:Approver Input(仅在 second qty 存在时显示) */} | |||
| {hasSecond && ( | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Radio | |||
| @@ -372,22 +394,42 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| type="number" | |||
| value={approverQty[detail.id] || ""} | |||
| 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"} | |||
| /> | |||
| <Typography variant="body2">-</Typography> | |||
| <TextField | |||
| size="small" | |||
| type="number" | |||
| value={approverBadQty[detail.id] || ""} | |||
| 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"} | |||
| /> | |||
| <Typography variant="body2"> | |||
| ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} | |||
| </Typography> | |||
| </Stack> | |||
| )} | |||
| {/* 差异行:显示 selected qty - bookqty = result */} | |||
| {(() => { | |||
| let selectedQty = 0; | |||
| @@ -396,7 +438,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| } else if (selection === "second") { | |||
| selectedQty = detail.secondStockTakeQty || 0; | |||
| } 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; | |||
| @@ -404,7 +446,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| return ( | |||
| <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> | |||
| ); | |||
| })()} | |||
| @@ -431,6 +473,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| </TableCell> | |||
| <TableCell> | |||
| {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( | |||
| <Box> | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| @@ -440,8 +483,11 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| > | |||
| {t("ReStockTake")} | |||
| </Button> | |||
| </Box> | |||
| )} | |||
| <br/> | |||
| {detail.finalQty == null && ( | |||
| <Box> | |||
| <Button | |||
| size="small" | |||
| variant="contained" | |||
| @@ -450,6 +496,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({ | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| </Box> | |||
| )} | |||
| </TableCell> | |||
| </TableRow> | |||
| @@ -16,6 +16,7 @@ import { | |||
| } from "@mui/material"; | |||
| import { useState, useCallback, useEffect } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import duration from "dayjs/plugin/duration"; | |||
| import { | |||
| getStockTakeRecords, | |||
| AllPickedStockTakeListReponse, | |||
| @@ -33,7 +34,9 @@ interface PickerCardListProps { | |||
| const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => { | |||
| const { t } = useTranslation(["inventory", "common"]); | |||
| dayjs.extend(duration); | |||
| const PER_PAGE = 6; | |||
| const [loading, setLoading] = useState(false); | |||
| const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]); | |||
| const [page, setPage] = useState(0); | |||
| @@ -88,10 +91,70 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||
| if (statusLower === "completed") return "success"; | |||
| if (statusLower === "in_progress" || statusLower === "processing") return "primary"; | |||
| if (statusLower === "approving") return "info"; | |||
| if (statusLower === "stockTaking") return "primary"; | |||
| if (statusLower === "no_cycle") return "default"; | |||
| 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 => { | |||
| if (session.totalInventoryLotNumber === 0) return 0; | |||
| return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); | |||
| @@ -145,13 +208,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||
| <Typography variant="subtitle1" fontWeight={600}> | |||
| {t("Section")}: {session.stockTakeSession} | |||
| </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> | |||
| <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 }}> | |||
| {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} | |||
| {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} /> | |||
| </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> | |||
| {session.totalInventoryLotNumber > 0 && ( | |||
| <Box sx={{ mt: 2 }}> | |||
| @@ -172,7 +243,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||
| )} | |||
| </CardContent> | |||
| <CardActions sx={{ pt: 0.5 }}> | |||
| <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}> | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| variant="contained" | |||
| size="small" | |||
| @@ -184,9 +256,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT | |||
| variant="contained" | |||
| size="small" | |||
| onClick={() => onReStockTakeClick(session)} | |||
| disabled={!session.reStockTakeTrueFalse} | |||
| > | |||
| {t("View ReStockTake")} | |||
| </Button> | |||
| </Stack> | |||
| <Chip size="small" label={t(session.status || "")} color={statusColor as any} /> | |||
| </CardActions> | |||
| </Card> | |||
| </Grid> | |||
| @@ -292,14 +292,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| } | |||
| return false; | |||
| }, []); | |||
| const uniqueWarehouses = Array.from( | |||
| new Set( | |||
| inventoryLotDetails | |||
| .map(detail => detail.warehouse) | |||
| .filter(warehouse => warehouse && warehouse.trim() !== "") | |||
| ) | |||
| ).join(", "); | |||
| return ( | |||
| <Box> | |||
| <Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||
| {uniqueWarehouses && ( | |||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||
| )} | |||
| </Typography> | |||
| {/* | |||
| {shortcutInput && ( | |||
| @@ -320,17 +330,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TableHead> | |||
| <TableRow> | |||
| <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("Bad Qty")}</TableCell> | |||
| {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} | |||
| <TableCell>{t("Remark")}</TableCell> | |||
| <TableCell>{t("UOM")}</TableCell> | |||
| <TableCell>{t("Status")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| <TableCell>{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -353,29 +361,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| return ( | |||
| <TableRow key={detail.id}> | |||
| <TableCell>{detail.warehouseCode || "-"}</TableCell> | |||
| <TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell> | |||
| <TableCell sx={{ | |||
| maxWidth: 100, | |||
| maxWidth: 150, | |||
| 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> | |||
| {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> | |||
| @@ -418,73 +416,69 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| </Stack> | |||
| </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> | |||
| {detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||
| @@ -18,8 +18,8 @@ import { | |||
| } from "@mui/material"; | |||
| import { useState, useCallback, useEffect, useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| AllPickedStockTakeListReponse, | |||
| import { | |||
| AllPickedStockTakeListReponse, | |||
| getInventoryLotDetailsBySection, | |||
| InventoryLotDetailResponse, | |||
| saveStockTakeRecord, | |||
| @@ -51,6 +51,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| // 编辑状态 | |||
| const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null); | |||
| // firstQty / secondQty 保存的是 total = available + bad | |||
| const [firstQty, setFirstQty] = useState<string>(""); | |||
| const [secondQty, setSecondQty] = useState<string>(""); | |||
| const [firstBadQty, setFirstBadQty] = useState<string>(""); | |||
| @@ -84,8 +85,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { | |||
| 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() || ""); | |||
| setSecondBadQty(detail.secondBadQty?.toString() || ""); | |||
| setRemark(detail.remarks || ""); | |||
| @@ -100,125 +112,164 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| 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(() => { | |||
| handleBatchSubmitAllRef.current = handleBatchSubmitAll; | |||
| @@ -227,11 +278,12 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| useEffect(() => { | |||
| const handleKeyPress = (e: KeyboardEvent) => { | |||
| 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; | |||
| } | |||
| @@ -240,48 +292,48 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| } | |||
| if (e.key.length === 1) { | |||
| setShortcutInput(prev => { | |||
| setShortcutInput((prev) => { | |||
| const newInput = prev + e.key; | |||
| if (newInput === '{2fitestall}') { | |||
| console.log('✅ Shortcut {2fitestall} detected!'); | |||
| if (newInput === "{2fitestall}") { | |||
| console.log("✅ Shortcut {2fitestall} detected!"); | |||
| setTimeout(() => { | |||
| 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 { | |||
| console.error('handleBatchSubmitAllRef.current is null'); | |||
| console.error("handleBatchSubmitAllRef.current is null"); | |||
| } | |||
| }, 0); | |||
| return ""; | |||
| } | |||
| if (newInput.length > 15) { | |||
| return ""; | |||
| } | |||
| if (newInput.length > 0 && !newInput.startsWith('{')) { | |||
| if (newInput.length > 0 && !newInput.startsWith("{")) { | |||
| return ""; | |||
| } | |||
| if (newInput.length > 5 && !newInput.startsWith('{2fi')) { | |||
| if (newInput.length > 5 && !newInput.startsWith("{2fi")) { | |||
| return ""; | |||
| } | |||
| 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(""); | |||
| } | |||
| }; | |||
| window.addEventListener('keydown', handleKeyPress); | |||
| window.addEventListener("keydown", handleKeyPress); | |||
| return () => { | |||
| window.removeEventListener('keydown', handleKeyPress); | |||
| window.removeEventListener("keydown", handleKeyPress); | |||
| }; | |||
| }, []); | |||
| @@ -292,19 +344,46 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| return false; | |||
| }, []); | |||
| const uniqueWarehouses = Array.from( | |||
| new Set( | |||
| inventoryLotDetails | |||
| .map((detail) => detail.warehouse) | |||
| .filter((warehouse) => warehouse && warehouse.trim() !== "") | |||
| ) | |||
| ).join(", "); | |||
| return ( | |||
| <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")} | |||
| </Button> | |||
| <Typography variant="h6" sx={{ mb: 2 }}> | |||
| {t("Stock Take Section")}: {selectedSession.stockTakeSession} | |||
| {uniqueWarehouses && ( | |||
| <> {t("Warehouse")}: {uniqueWarehouses}</> | |||
| )} | |||
| </Typography> | |||
| {/* 如果需要显示快捷键输入,可以把这块注释打开 */} | |||
| {/* | |||
| {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}> | |||
| {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> | |||
| </Box> | |||
| )} | |||
| @@ -319,17 +398,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TableHead> | |||
| <TableRow> | |||
| <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("Status")}</TableCell> | |||
| <TableCell>{t("Record Status")}</TableCell> | |||
| <TableCell>{t("Action")}</TableCell> | |||
| </TableRow> | |||
| @@ -337,7 +409,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| <TableBody> | |||
| {inventoryLotDetails.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={12} align="center"> | |||
| <TableCell colSpan={7} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data")} | |||
| </Typography> | |||
| @@ -347,152 +419,215 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| inventoryLotDetails.map((detail) => { | |||
| const isEditing = editingRecord?.id === detail.id; | |||
| 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 ( | |||
| <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> | |||
| {detail.expiryDate | |||
| ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) | |||
| : "-"} | |||
| {detail.warehouseArea || "-"} | |||
| {detail.warehouseSlot || "-"} | |||
| </TableCell> | |||
| <TableCell> | |||
| <TableCell | |||
| sx={{ | |||
| maxWidth: 150, | |||
| wordBreak: "break-word", | |||
| whiteSpace: "normal", | |||
| lineHeight: 1.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> | |||
| </TableCell> | |||
| <TableCell> | |||
| <Stack spacing={0.5}> | |||
| {/* Qty + Bad Qty 合并显示/输入 */} | |||
| <TableCell sx={{ minWidth: 300 }}> | |||
| <Stack spacing={1}> | |||
| {/* First */} | |||
| {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"> | |||
| {t("First")}: {detail.firstBadQty.toFixed(2)} | |||
| {t("First")}:{" "} | |||
| {formatNumber( | |||
| (detail.firstStockTakeQty ?? 0) + | |||
| (detail.firstBadQty ?? 0) | |||
| )}{" "} | |||
| ( | |||
| {formatNumber( | |||
| detail.firstBadQty ?? 0 | |||
| )} | |||
| ) ={" "} | |||
| {formatNumber(detail.firstStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : null} | |||
| {/* Second */} | |||
| {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"> | |||
| {t("Second")}: {detail.secondBadQty.toFixed(2)} | |||
| {t("Second")}:{" "} | |||
| {formatNumber( | |||
| (detail.secondStockTakeQty ?? 0) + | |||
| (detail.secondBadQty ?? 0) | |||
| )}{" "} | |||
| ( | |||
| {formatNumber( | |||
| detail.secondBadQty ?? 0 | |||
| )} | |||
| ) ={" "} | |||
| {formatNumber(detail.secondStockTakeQty ?? 0)} | |||
| </Typography> | |||
| ) : 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> | |||
| </TableCell> | |||
| {/* Remark */} | |||
| <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>{detail.uom || "-"}</TableCell> | |||
| <TableCell> | |||
| {detail.stockTakeRecordStatus === "pass" ? ( | |||
| <Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" /> | |||
| <Chip | |||
| size="small" | |||
| label={t(detail.stockTakeRecordStatus)} | |||
| color="success" | |||
| /> | |||
| ) : 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> | |||
| {isEditing ? ( | |||
| <Stack direction="row" spacing={1}> | |||
| @@ -504,13 +639,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| > | |||
| {t("Save")} | |||
| </Button> | |||
| <Button | |||
| size="small" | |||
| onClick={handleCancelEdit} | |||
| > | |||
| <Button size="small" onClick={handleCancelEdit}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| ) : ( | |||
| <Button | |||
| @@ -519,10 +650,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({ | |||
| onClick={() => handleStartEdit(detail)} | |||
| disabled={submitDisabled} | |||
| > | |||
| {!detail.stockTakeRecordId | |||
| ? t("Input") | |||
| : detail.stockTakeRecordStatus === "notMatch" | |||
| ? t("Input") | |||
| {!detail.stockTakeRecordId | |||
| ? t("Input") | |||
| : detail.stockTakeRecordStatus === "notMatch" | |||
| ? t("Input") | |||
| : t("View")} | |||
| </Button> | |||
| )} | |||
| @@ -1,13 +1,13 @@ | |||
| import React from "react"; | |||
| import GeneralLoading from "../General/GeneralLoading"; | |||
| import StockTakeManagement from "./StockTakeManagement"; | |||
| import StockTakeTabs from "./StockTakeTab"; | |||
| interface SubComponents { | |||
| Loading: typeof GeneralLoading; | |||
| } | |||
| const StockTakeManagementWrapper: React.FC & SubComponents = async () => { | |||
| return <StockTakeManagement />; | |||
| return <StockTakeTabs/>; | |||
| }; | |||
| StockTakeManagementWrapper.Loading = GeneralLoading; | |||
| @@ -4,7 +4,7 @@ | |||
| "Job Order Production Process": "工單生產流程", | |||
| "productionProcess": "生產流程", | |||
| "Search Criteria": "搜尋條件", | |||
| "All": "全部", | |||
| "Stock Record": "庫存記錄", | |||
| "No options": "沒有選項", | |||
| "Select Another Bag Lot": "選擇另一個包裝袋", | |||
| "Finished QC Job Orders": "完成QC工單", | |||
| @@ -28,6 +28,13 @@ | |||
| "Total finished QC job orders": "總完成QC工單數量", | |||
| "Over Time": "超時", | |||
| "Code": "編號", | |||
| "Job Order No.": "工單編號", | |||
| "FG / WIP Item": "成品/半成品", | |||
| "Production Time Remaining": "生產剩餘時間", | |||
| "Process": "工序", | |||
| "Start": "開始", | |||
| "Finish": "完成", | |||
| "Wait Time [minutes]": "等待時間(分鐘)", | |||
| "Staff No": "員工編號", | |||
| "code": "編號", | |||
| "Name": "名稱", | |||
| @@ -41,6 +48,8 @@ | |||
| "No": "沒有", | |||
| "Assignment failed: ": "分配失敗: ", | |||
| "Unknown error": "未知錯誤", | |||
| "Job Process Status": "工單流程狀態", | |||
| "FG / WIP Item": "成品/半成品", | |||
| "WIP": "半成品", | |||
| "R&D": "研發", | |||
| "STF": "樣品", | |||
| @@ -10,12 +10,29 @@ | |||
| "fg": "成品", | |||
| "Back to List": "返回列表", | |||
| "Record Status": "記錄狀態", | |||
| "Stock take record status updated to not match": "盤點記錄狀態更新為數值不符", | |||
| "available": "可用", | |||
| "Item-lotNo-ExpiryDate": "貨品-批號-到期日", | |||
| "not available": "不可用", | |||
| "Batch Submit All": "批量提交所有", | |||
| "Batch Save All": "批量保存所有", | |||
| "not match": "數值不符", | |||
| "Stock Take Qty": "盤點數量(含壞盤點數量)", | |||
| "Stock Take Qty(include Bad Qty)= Available Qty": "盤點數(含壞品)= 可用數", | |||
| "View ReStockTake": "查看重新盤點", | |||
| "Stock Take Qty": "盤點數", | |||
| "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": "通過", | |||
| "not pass": "不通過", | |||
| "Available": "可用", | |||
| @@ -23,7 +40,7 @@ | |||
| "pending": "待處理", | |||
| "Last Stock Take Date": "上次盤點日期", | |||
| "Remark": "備註", | |||
| "notMatch": "不匹配", | |||
| "notMatch": "數值不符", | |||
| "Stock take record saved successfully": "盤點記錄保存成功", | |||
| "View Details": "查看詳細", | |||
| "Input": "輸入", | |||
| @@ -133,5 +150,17 @@ | |||
| "Stock take adjustment confirmed! (Demo only)": "盤點調整確認!(僅演示)", | |||
| "Stock take adjustment has been confirmed successfully!": "盤點調整確認成功!", | |||
| "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": "工單提料", | |||
| "Pick Order Detail": "提料單細節", | |||
| "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": "編號", | |||
| "Route": "路線", | |||
| "Qty": "數量", | |||
| @@ -517,6 +524,13 @@ | |||
| "Start 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": "完成" | |||
| } | |||