CANCERYS\kw093 3 недель назад
Родитель
Сommit
69a3fcb7ae
13 измененных файлов: 803 добавлений и 425 удалений
  1. +4
    -0
      src/app/api/stockTake/actions.ts
  2. +10
    -10
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  3. +5
    -0
      src/components/NavigationContent/NavigationContent.tsx
  4. +4
    -4
      src/components/PickOrderSearch/PickExecution.tsx
  5. +78
    -8
      src/components/StockTakeManagement/ApproverCardList.tsx
  6. +64
    -17
      src/components/StockTakeManagement/ApproverStockTake.tsx
  7. +80
    -5
      src/components/StockTakeManagement/PickerCardList.tsx
  8. +85
    -91
      src/components/StockTakeManagement/PickerReStockTake.tsx
  9. +413
    -282
      src/components/StockTakeManagement/PickerStockTake.tsx
  10. +2
    -2
      src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
  11. +10
    -1
      src/i18n/zh/common.json
  12. +32
    -3
      src/i18n/zh/inventory.json
  13. +16
    -2
      src/i18n/zh/jo.json

+ 4
- 0
src/app/api/stockTake/actions.ts Просмотреть файл

@@ -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) => {


+ 10
- 10
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Просмотреть файл

@@ -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 数据


+ 5
- 0
src/components/NavigationContent/NavigationContent.tsx Просмотреть файл

@@ -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",
},
], ],
}, },
// { // {


+ 4
- 4
src/components/PickOrderSearch/PickExecution.tsx Просмотреть файл

@@ -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;
} }
} }


+ 78
- 8
src/components/StockTakeManagement/ApproverCardList.tsx Просмотреть файл

@@ -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>


+ 64
- 17
src/components/StockTakeManagement/ApproverStockTake.tsx Просмотреть файл

@@ -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>


+ 80
- 5
src/components/StockTakeManagement/PickerCardList.tsx Просмотреть файл

@@ -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>


+ 85
- 91
src/components/StockTakeManagement/PickerReStockTake.tsx Просмотреть файл

@@ -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" />


+ 413
- 282
src/components/StockTakeManagement/PickerStockTake.tsx Просмотреть файл

@@ -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>
)} )}


+ 2
- 2
src/components/StockTakeManagement/StockTakeManagementWrapper.tsx Просмотреть файл

@@ -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;


+ 10
- 1
src/i18n/zh/common.json Просмотреть файл

@@ -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": "樣品",


+ 32
- 3
src/i18n/zh/inventory.json Просмотреть файл

@@ -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": "正常"

} }

+ 16
- 2
src/i18n/zh/jo.json Просмотреть файл

@@ -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": "完成"
} }

Загрузка…
Отмена
Сохранить