diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts
index 5e0c0e6..c092195 100644
--- a/src/app/api/stockTake/actions.ts
+++ b/src/app/api/stockTake/actions.ts
@@ -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) => {
diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
index 99b1d6f..56a41e4 100644
--- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
+++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
@@ -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 数据
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index a614269..861e922 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -106,6 +106,11 @@ const NavigationContent: React.FC = () => {
label: "Finished Good Order",
path: "/finishedGood",
},
+ {
+ icon: ,
+ label: "Stock Record",
+ path: "/stockRecord",
+ },
],
},
// {
diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx
index 486b52e..50f6d92 100644
--- a/src/components/PickOrderSearch/PickExecution.tsx
+++ b/src/components/PickOrderSearch/PickExecution.tsx
@@ -362,8 +362,8 @@ const PickExecution: React.FC = ({ 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 = ({ 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;
}
}
diff --git a/src/components/StockTakeManagement/ApproverCardList.tsx b/src/components/StockTakeManagement/ApproverCardList.tsx
index 1baad2a..153f5a7 100644
--- a/src/components/StockTakeManagement/ApproverCardList.tsx
+++ b/src/components/StockTakeManagement/ApproverCardList.tsx
@@ -58,7 +58,66 @@ const ApproverCardList: React.FC = ({ 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 = ({ onCardClick }) => {
{t("Section")}: {session.stockTakeSession}
- {session.status ? (
-
- ) : (
-
- )}
+
+ {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
+
+
+ {t("Stock Taker")}: {session.stockTakerName || "-"}
+ {t("Approver")}: {session.approverName || "-"}
+
+
+ {t("start time")}: {startTimeDisplay(session.startTime) || "-"}
+ {t("end time")}: {endTimeDisplay(session.endTime) || "-"}
+
- {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
+ {t("Control Time")}:
-
{session.totalInventoryLotNumber > 0 && (
@@ -156,7 +220,7 @@ const ApproverCardList: React.FC = ({ onCardClick }) => {
)}
-
+
+ {session.status ? (
+
+ ) : (
+
+ )}
diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx
index 4426965..a036bd0 100644
--- a/src/components/StockTakeManagement/ApproverStockTake.tsx
+++ b/src/components/StockTakeManagement/ApproverStockTake.tsx
@@ -252,7 +252,20 @@ const ApproverStockTake: React.FC = ({
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 = ({
+
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
+ {uniqueWarehouses && (
+ <> {t("Warehouse")}: {uniqueWarehouses}>
+ )}
+
+
+
{loadingDetails ? (
@@ -279,8 +301,8 @@ const ApproverStockTake: React.FC = ({
{t("Warehouse Location")}
- {t("Item")}
- {t("Stock Take Qty")}
+ {t("Item-lotNo-ExpiryDate")}
+ {t("Stock Take Qty(include Bad Qty)= Available Qty")}
{t("Remark")}
{t("UOM")}
{t("Record Status")}
@@ -316,21 +338,21 @@ const ApproverStockTake: React.FC = ({
{detail.itemCode || "-"} {detail.itemName || "-"}
{detail.lotNo || "-"}
{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}
-
+ {/**/}
{detail.finalQty != null ? (
- // 提交后只显示差异行
+
- {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))}
) : (
- {/* 第一行:First Qty(默认选中) */}
+
{hasFirst && (
= ({
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })}
/>
- {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)}
)}
- {/* 第二行:Second Qty(如果存在) */}
+
{hasSecond && (
= ({
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })}
/>
- {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)}
)}
- {/* 第三行:Approver Input(仅在 second qty 存在时显示) */}
+
{hasSecond && (
= ({
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"}
/>
- -
+
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"}
/>
+
+ ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
+
)}
- {/* 差异行:显示 selected qty - bookqty = result */}
+
{(() => {
let selectedQty = 0;
@@ -396,7 +438,7 @@ const ApproverStockTake: React.FC = ({
} 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 = ({
return (
- {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)}
);
})()}
@@ -431,6 +473,7 @@ const ApproverStockTake: React.FC = ({
{detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && (
+
+
)}
+
{detail.finalQty == null && (
+
+
)}
diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx
index 9b10871..a6affe8 100644
--- a/src/components/StockTakeManagement/PickerCardList.tsx
+++ b/src/components/StockTakeManagement/PickerCardList.tsx
@@ -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 = ({ onCardClick, onReStockTakeClick }) => {
const { t } = useTranslation(["inventory", "common"]);
+ dayjs.extend(duration);
+ const PER_PAGE = 6;
const [loading, setLoading] = useState(false);
const [stockTakeSessions, setStockTakeSessions] = useState([]);
const [page, setPage] = useState(0);
@@ -88,10 +91,70 @@ const PickerCardList: React.FC = ({ 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 = ({ onCardClick, onReStockT
{t("Section")}: {session.stockTakeSession}
-
+
+ {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
+
+
+ {t("Stock Taker")}: {session.stockTakerName}
+
+
+ {t("start time")}: {startTimeDisplay(session.startTime) || "-"}
+ {t("end time")}: {endTimeDisplay(session.endTime) || "-"}
+
- {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
+ {t("Control Time")}:
- {t("Stock Taker")}: {session.stockTakerName}
{t("Total Item Number")}: {session.totalItemNumber}
{session.totalInventoryLotNumber > 0 && (
@@ -172,7 +243,8 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT
)}
-
+
+
+
+
diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx
index fee9a6b..e47dbe8 100644
--- a/src/components/StockTakeManagement/PickerReStockTake.tsx
+++ b/src/components/StockTakeManagement/PickerReStockTake.tsx
@@ -292,14 +292,24 @@ const PickerStockTake: React.FC = ({
}
return false;
}, []);
-
+
+ const uniqueWarehouses = Array.from(
+ new Set(
+ inventoryLotDetails
+ .map(detail => detail.warehouse)
+ .filter(warehouse => warehouse && warehouse.trim() !== "")
+ )
+ ).join(", ");
return (
- {t("Stock Take Section")}: {selectedSession.stockTakeSession}
+ {t("Stock Take Section")}: {selectedSession.stockTakeSession}
+ {uniqueWarehouses && (
+ <> {t("Warehouse")}: {uniqueWarehouses}>
+ )}
{/*
{shortcutInput && (
@@ -320,17 +330,15 @@ const PickerStockTake: React.FC = ({
{t("Warehouse Location")}
- {t("Item")}
- {/*{t("Item Name")}*/}
- {/*{t("Lot No")}*/}
- {t("Expiry Date")}
+ {t("Item-lotNo-ExpiryDate")}
+
{t("Qty")}
{t("Bad Qty")}
{/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
{t("Remark")}
{t("UOM")}
- {t("Status")}
+
{t("Record Status")}
{t("Action")}
@@ -353,29 +361,19 @@ const PickerStockTake: React.FC = ({
return (
- {detail.warehouseCode || "-"}
+ {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}
{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}
- {/*
-
- {detail.itemName || "-"}
- */}
- {/*{detail.lotNo || "-"}*/}
-
- {detail.expiryDate
- ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
- : "-"}
+ }}>
+
+ {detail.itemCode || "-"} {detail.itemName || "-"}
+ {detail.lotNo || "-"}
+ {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}
+ {/**/}
+
@@ -418,73 +416,69 @@ const PickerStockTake: React.FC = ({
-
- {isEditing && isFirstSubmit ? (
- setFirstBadQty(e.target.value)}
- sx={{ width: 100 }}
-
- />
- ) : detail.firstBadQty ? (
-
- {t("First")}: {detail.firstBadQty.toFixed(2)}
-
- ) : null}
-
- {isEditing && isSecondSubmit ? (
- setSecondBadQty(e.target.value)}
- sx={{ width: 100 }}
-
- />
- ) : detail.secondBadQty ? (
-
- {t("Second")}: {detail.secondBadQty.toFixed(2)}
-
- ) : null}
-
- {!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
-
- -
-
- )}
-
-
-
-
- {isEditing && isSecondSubmit ? (
- <>
- {t("Remark")}
- setRemark(e.target.value)}
- sx={{ width: 150 }}
- // If you want a single-line input, remove multiline/rows:
- // multiline
- // rows={2}
- />
- >
- ) : (
-
- {detail.remarks || "-"}
-
- )}
-
- {detail.uom || "-"}
-
- {detail.status ? (
-
+
+ {isEditing && isFirstSubmit ? (
+ setFirstBadQty(e.target.value)}
+ sx={{ width: 100 }}
+ />
+ ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? (
+
+ {t("First")}: {detail.firstBadQty.toFixed(2)}
+
) : (
- "-"
+
+
+ {t("First")}: 0.00
+
)}
-
+
+ {isEditing && isSecondSubmit ? (
+ setSecondBadQty(e.target.value)}
+ sx={{ width: 100 }}
+ />
+ ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? (
+
+ {t("Second")}: {detail.secondBadQty.toFixed(2)}
+
+ ) : null}
+
+ {!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
+
+ -
+
+ )}
+
+
+
+ {isEditing && isSecondSubmit ? (
+ <>
+ {t("Remark")}
+ setRemark(e.target.value)}
+ sx={{ width: 150 }}
+ // If you want a single-line input, remove multiline/rows:
+ // multiline
+ // rows={2}
+ />
+ >
+ ) : (
+
+ {detail.remarks || "-"}
+
+ )}
+
+ {detail.uom || "-"}
+
{detail.stockTakeRecordStatus === "pass" ? (
diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx
index d480718..e1dfa1b 100644
--- a/src/components/StockTakeManagement/PickerStockTake.tsx
+++ b/src/components/StockTakeManagement/PickerStockTake.tsx
@@ -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 = ({
// 编辑状态
const [editingRecord, setEditingRecord] = useState(null);
+ // firstQty / secondQty 保存的是 total = available + bad
const [firstQty, setFirstQty] = useState("");
const [secondQty, setSecondQty] = useState("");
const [firstBadQty, setFirstBadQty] = useState("");
@@ -84,8 +85,19 @@ const PickerStockTake: React.FC = ({
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 = ({
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 = ({
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 = ({
}
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 = ({
return false;
}, []);
+ const uniqueWarehouses = Array.from(
+ new Set(
+ inventoryLotDetails
+ .map((detail) => detail.warehouse)
+ .filter((warehouse) => warehouse && warehouse.trim() !== "")
+ )
+ ).join(", ");
+
return (
-
-
+
{t("Cancel")}
-
) : (
= ({
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")}
)}
diff --git a/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx b/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
index d306255..c39bc1f 100644
--- a/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
+++ b/src/components/StockTakeManagement/StockTakeManagementWrapper.tsx
@@ -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 ;
+ return ;
};
StockTakeManagementWrapper.Loading = GeneralLoading;
diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json
index d7d8f61..901f62c 100644
--- a/src/i18n/zh/common.json
+++ b/src/i18n/zh/common.json
@@ -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": "樣品",
diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json
index bb0f8da..6e63f05 100644
--- a/src/i18n/zh/inventory.json
+++ b/src/i18n/zh/inventory.json
@@ -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": "正常"
+
}
diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json
index 3144af4..721fee0 100644
--- a/src/i18n/zh/jo.json
+++ b/src/i18n/zh/jo.json
@@ -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": "完成"
}