Kaynağa Gözat

update

master
CANCERYS\kw093 3 hafta önce
ebeveyn
işleme
69a3fcb7ae
13 değiştirilmiş dosya ile 803 ekleme ve 425 silme
  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 Dosyayı Görüntüle

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


+ 10
- 10
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Dosyayı Görüntüle

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


+ 5
- 0
src/components/NavigationContent/NavigationContent.tsx Dosyayı Görüntüle

@@ -106,6 +106,11 @@ const NavigationContent: React.FC = () => {
label: "Finished Good Order",
path: "/finishedGood",
},
{
icon: <RequestQuote />,
label: "Stock Record",
path: "/stockRecord",
},
],
},
// {


+ 4
- 4
src/components/PickOrderSearch/PickExecution.tsx Dosyayı Görüntüle

@@ -362,8 +362,8 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
// FIXED: 计算累计拣货数量
const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty;
console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0);
console.log("🔍 DEBUG - Current submit:", qty);
console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot);
console.log(" DEBUG - Current submit:", qty);
console.log(" DEBUG - Total picked:", totalPickedForThisLot);
console.log("�� DEBUG - Required qty:", selectedLot.requiredQty);
// FIXED: 状态应该基于累计拣货数量
@@ -428,7 +428,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
if (currentConsoCode) {
try {
console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`);
console.log(` Checking completion for consoCode: ${currentConsoCode}`);
const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode);
console.log("�� Completion response:", completionResponse);
@@ -788,7 +788,7 @@ const handleIssueNoLotStockOutLine = useCallback(async (stockOutLineId: number)
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
correctConsoCode = pickOrder.consoCode;
console.log(`🔍 Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`);
console.log(` Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`);
break;
}
}


+ 78
- 8
src/components/StockTakeManagement/ApproverCardList.tsx Dosyayı Görüntüle

@@ -58,7 +58,66 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => {

const startIdx = page * PER_PAGE;
const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => {
const [currentTime, setCurrentTime] = useState(dayjs());
useEffect(() => {
if (!endTime && startTime) {
const interval = setInterval(() => {
setCurrentTime(dayjs());
}, 1000); // 每秒更新一次
return () => clearInterval(interval);
}
}, [startTime, endTime]);
if (endTime && startTime) {
// 当有结束时间时,计算从开始到结束的持续时间
const start = dayjs(startTime);
const end = dayjs(endTime);
const duration = dayjs.duration(end.diff(start));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
return (
<>
{hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</>
);
} else if (startTime) {
// 当没有结束时间时,显示实时计时器
const start = dayjs(startTime);
const duration = dayjs.duration(currentTime.diff(start));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
return (
<>
{hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</>
);
} else {
return <>-</>;
}
};
const startTimeDisplay = (startTime: string | null) => {
if (startTime) {
const start = dayjs(startTime);
return start.format("HH:mm");
} else {
return "-";
}
};
const endTimeDisplay = (endTime: string | null) => {
if (endTime) {
const end = dayjs(endTime);
return end.format("HH:mm");
} else {
return "-";
}
};
const getStatusColor = (status: string | null) => {
if (!status) return "default";
@@ -126,17 +185,22 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => {
<Typography variant="subtitle1" fontWeight={600}>
{t("Section")}: {session.stockTakeSession}
</Typography>
{session.status ? (
<Chip size="small" label={t(session.status)} color={statusColor as any} />
) : (
<Chip size="small" label={t(" ")} color="default" />
)}
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
</Typography>
</Stack>

<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName || "-"}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Approver")}: {session.approverName || "-"}</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
{t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
</Typography>

{session.totalInventoryLotNumber > 0 && (
<Box sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}>
@@ -156,7 +220,7 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => {
)}
</CardContent>

<CardActions sx={{ pt: 0.5 }}>
<CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>
<Button
variant="contained"
size="small"
@@ -169,7 +233,13 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => {
}}
>
{t("View Details")}
</Button>
{session.status ? (
<Chip size="small" label={t(session.status)} color={statusColor as any} />
) : (
<Chip size="small" label={t(" ")} color="default" />
)}
</CardActions>
</Card>
</Grid>


+ 64
- 17
src/components/StockTakeManagement/ApproverStockTake.tsx Dosyayı Görüntüle

@@ -252,7 +252,20 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
useEffect(() => {
handleBatchSubmitAllRef.current = handleBatchSubmitAll;
}, [handleBatchSubmitAll]);

const formatNumber = (num: number | null | undefined): string => {
if (num == null) return "0.00";
return num.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
};
const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
.map(detail => detail.warehouse)
.filter(warehouse => warehouse && warehouse.trim() !== "")
)
).join(", ");
const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => {
// Only allow editing if there's a first stock take qty
if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) {
@@ -266,9 +279,18 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
<Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
{t("Back to List")}
</Button>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>

<Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
{t("Batch Save All")}
</Button>
</Stack>
{loadingDetails ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
@@ -279,8 +301,8 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item")}</TableCell>
<TableCell>{t("Stock Take Qty")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
@@ -316,21 +338,21 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>
{/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/}
</Stack>
</TableCell>
<TableCell sx={{ minWidth: 300 }}>
{detail.finalQty != null ? (
// 提交后只显示差异行
<Stack spacing={0.5}>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t("Difference")}: {detail.finalQty?.toFixed(2) || "0.00"} - {(detail.availableQty || 0).toFixed(2)} = {((detail.finalQty || 0) - (detail.availableQty || 0)).toFixed(2)}
{t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber((detail.finalQty || 0) - (detail.availableQty || 0))}
</Typography>
</Stack>
) : (
<Stack spacing={1}>
{/* 第一行:First Qty(默认选中) */}
{hasFirst && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
@@ -339,12 +361,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })}
/>
<Typography variant="body2">
{t("First")}: {(detail.firstStockTakeQty??0)+(detail.firstBadQty??0) || "0.00"} ({detail.firstBadQty??0})
{t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)}
</Typography>
</Stack>
)}
{/* 第二行:Second Qty(如果存在) */}
{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
@@ -353,12 +375,12 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })}
/>
<Typography variant="body2">
{t("Second")}: {(detail.secondStockTakeQty??0)+(detail.secondBadQty??0) || "0.00"} ({detail.secondBadQty??0})
{t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)}
</Typography>
</Stack>
)}
{/* 第三行:Approver Input(仅在 second qty 存在时显示) */}
{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
@@ -372,22 +394,42 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
type="number"
value={approverQty[detail.id] || ""}
onChange={(e) => setApproverQty({ ...approverQty, [detail.id]: e.target.value })}
sx={{ width: 100 }}
sx={{
width: 130,
minWidth: 130,
'& .MuiInputBase-input': {
height: '1.4375em',
padding: '4px 8px'
}
}}
placeholder={t("Stock Take Qty") }
disabled={selection !== "approver"}
/>
<Typography variant="body2">-</Typography>
<TextField
size="small"
type="number"
value={approverBadQty[detail.id] || ""}
onChange={(e) => setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })}
sx={{ width: 100 }}
sx={{
width: 130,
minWidth: 130,
'& .MuiInputBase-input': {
height: '1.4375em',
padding: '4px 8px'
}
}}
placeholder={t("Bad Qty")}
disabled={selection !== "approver"}
/>
<Typography variant="body2">
={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
</Typography>
</Stack>
)}
{/* 差异行:显示 selected qty - bookqty = result */}
{(() => {
let selectedQty = 0;
@@ -396,7 +438,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
} else if (selection === "second") {
selectedQty = detail.secondStockTakeQty || 0;
} else if (selection === "approver") {
selectedQty = parseFloat(approverQty[detail.id] || "0") || 0;
selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0;
}
const bookQty = detail.availableQty || 0;
@@ -404,7 +446,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
return (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t("Difference")}: {selectedQty.toFixed(2)} - {bookQty.toFixed(2)} = {difference.toFixed(2)}
{t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)}
</Typography>
);
})()}
@@ -431,6 +473,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
</TableCell>
<TableCell>
{detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && (
<Box>
<Button
size="small"
variant="outlined"
@@ -440,8 +483,11 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
>
{t("ReStockTake")}
</Button>
</Box>
)}
<br/>
{detail.finalQty == null && (
<Box>
<Button
size="small"
variant="contained"
@@ -450,6 +496,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
>
{t("Save")}
</Button>
</Box>
)}
</TableCell>
</TableRow>


+ 80
- 5
src/components/StockTakeManagement/PickerCardList.tsx Dosyayı Görüntüle

@@ -16,6 +16,7 @@ import {
} from "@mui/material";
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import duration from "dayjs/plugin/duration";
import {
getStockTakeRecords,
AllPickedStockTakeListReponse,
@@ -33,7 +34,9 @@ interface PickerCardListProps {

const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => {
const { t } = useTranslation(["inventory", "common"]);
dayjs.extend(duration);

const PER_PAGE = 6;
const [loading, setLoading] = useState(false);
const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]);
const [page, setPage] = useState(0);
@@ -88,10 +91,70 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
if (statusLower === "completed") return "success";
if (statusLower === "in_progress" || statusLower === "processing") return "primary";
if (statusLower === "approving") return "info";
if (statusLower === "stockTaking") return "primary";
if (statusLower === "no_cycle") return "default";
return "warning";
};

const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => {
const [currentTime, setCurrentTime] = useState(dayjs());
useEffect(() => {
if (!endTime && startTime) {
const interval = setInterval(() => {
setCurrentTime(dayjs());
}, 1000); // 每秒更新一次
return () => clearInterval(interval);
}
}, [startTime, endTime]);
if (endTime && startTime) {
// 当有结束时间时,计算从开始到结束的持续时间
const start = dayjs(startTime);
const end = dayjs(endTime);
const duration = dayjs.duration(end.diff(start));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
return (
<>
{hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</>
);
} else if (startTime) {
// 当没有结束时间时,显示实时计时器
const start = dayjs(startTime);
const duration = dayjs.duration(currentTime.diff(start));
const hours = Math.floor(duration.asHours());
const minutes = duration.minutes();
const seconds = duration.seconds();
return (
<>
{hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
</>
);
} else {
return <>-</>;
}
};
const startTimeDisplay = (startTime: string | null) => {
if (startTime) {
const start = dayjs(startTime);
return start.format("HH:mm");
} else {
return "-";
}
};
const endTimeDisplay = (endTime: string | null) => {
if (endTime) {
const end = dayjs(endTime);
return end.format("HH:mm");
} else {
return "-";
}
};
const getCompletionRate = (session: AllPickedStockTakeListReponse): number => {
if (session.totalInventoryLotNumber === 0) return 0;
return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100);
@@ -145,13 +208,21 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
<Typography variant="subtitle1" fontWeight={600}>
{t("Section")}: {session.stockTakeSession}
</Typography>
<Chip size="small" label={t(session.status || "")} color={statusColor as any} />
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
</Typography>
</Stack>

<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
{t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography>
{session.totalInventoryLotNumber > 0 && (
<Box sx={{ mt: 2 }}>
@@ -172,7 +243,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
)}
</CardContent>

<CardActions sx={{ pt: 0.5 }}>
<CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
size="small"
@@ -184,9 +256,12 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
variant="contained"
size="small"
onClick={() => onReStockTakeClick(session)}
disabled={!session.reStockTakeTrueFalse}
>
{t("View ReStockTake")}
</Button>
</Stack>
<Chip size="small" label={t(session.status || "")} color={statusColor as any} />
</CardActions>
</Card>
</Grid>


+ 85
- 91
src/components/StockTakeManagement/PickerReStockTake.tsx Dosyayı Görüntüle

@@ -292,14 +292,24 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}
return false;
}, []);

const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
.map(detail => detail.warehouse)
.filter(warehouse => warehouse && warehouse.trim() !== "")
)
).join(", ");
return (
<Box>
<Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
{t("Back to List")}
</Button>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>
{/*
{shortcutInput && (
@@ -320,17 +330,15 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item")}</TableCell>
{/*<TableCell>{t("Item Name")}</TableCell>*/}
{/*<TableCell>{t("Lot No")}</TableCell>*/}
<TableCell>{t("Expiry Date")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Qty")}</TableCell>
<TableCell>{t("Bad Qty")}</TableCell>
{/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Status")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
@@ -353,29 +361,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseCode || "-"}</TableCell>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 100,
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell>
{/*
<TableCell
sx={{
maxWidth: 200,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}
>
{detail.itemName || "-"}
</TableCell>*/}
{/*<TableCell>{detail.lotNo || "-"}</TableCell>*/}
<TableCell>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
}}>
<Stack spacing={0.5}>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
{/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/}
</Stack>
</TableCell>
<TableCell>
@@ -418,73 +416,69 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
</Typography>
) : null}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
// If you want a single-line input, remove multiline/rows:
// multiline
// rows={2}
/>
</>
) : (
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell>
{detail.status ? (
<Chip size="small" label={t(detail.status)} color="default" />
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty != null && detail.firstBadQty > 0 ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
</Typography>
) : (
"-"
<Typography variant="body2" sx={{ visibility: 'hidden' }}>
{t("First")}: 0.00
</Typography>
)}
</TableCell>
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty != null && detail.secondBadQty > 0 ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
// If you want a single-line input, remove multiline/rows:
// multiline
// rows={2}
/>
</>
) : (
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />


+ 413
- 282
src/components/StockTakeManagement/PickerStockTake.tsx Dosyayı Görüntüle

@@ -18,8 +18,8 @@ import {
} from "@mui/material";
import { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
AllPickedStockTakeListReponse,
import {
AllPickedStockTakeListReponse,
getInventoryLotDetailsBySection,
InventoryLotDetailResponse,
saveStockTakeRecord,
@@ -51,6 +51,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
// 编辑状态
const [editingRecord, setEditingRecord] = useState<InventoryLotDetailResponse | null>(null);
// firstQty / secondQty 保存的是 total = available + bad
const [firstQty, setFirstQty] = useState<string>("");
const [secondQty, setSecondQty] = useState<string>("");
const [firstBadQty, setFirstBadQty] = useState<string>("");
@@ -84,8 +85,19 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({

const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => {
setEditingRecord(detail);
setFirstQty(detail.firstStockTakeQty?.toString() || "");
setSecondQty(detail.secondStockTakeQty?.toString() || "");

// 编辑时,输入 total = qty + badQty
const firstTotal =
detail.firstStockTakeQty != null
? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString()
: "";
const secondTotal =
detail.secondStockTakeQty != null
? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString()
: "";

setFirstQty(firstTotal);
setSecondQty(secondTotal);
setFirstBadQty(detail.firstBadQty?.toString() || "");
setSecondBadQty(detail.secondBadQty?.toString() || "");
setRemark(detail.remarks || "");
@@ -100,125 +112,164 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
setRemark("");
}, []);

const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
return;
}
const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
const qty = isFirstSubmit ? firstQty : secondQty;
const badQty = isFirstSubmit ? firstBadQty : secondBadQty;
if (!qty || !badQty) {
onSnackbar(
isFirstSubmit
? t("Please enter QTY and Bad QTY")
: t("Please enter Second QTY and Bad QTY"),
"error"
);
return;
}
setSaving(true);
try {
const request: SaveStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
inventoryLotLineId: detail.id,
qty: parseFloat(qty),
badQty: parseFloat(badQty),
remark: isSecondSubmit ? (remark || null) : null,
};
console.log('handleSaveStockTake: request:', request);
console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId);
console.log('handleSaveStockTake: currentUserId:', currentUserId);
await saveStockTakeRecord(
request,
selectedSession.stockTakeId,
currentUserId
);
onSnackbar(t("Stock take record saved successfully"), "success");
handleCancelEdit();
const details = await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
);
setInventoryLotDetails(Array.isArray(details) ? details : []);
} catch (e: any) {
console.error("Save stock take record error:", e);
let errorMessage = t("Failed to save stock take record");
if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// ignore
const formatNumber = (num: number | null | undefined): string => {
if (num == null || Number.isNaN(num)) return "0.00";
return num.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
};

const handleSaveStockTake = useCallback(
async (detail: InventoryLotDetailResponse) => {
if (!selectedSession || !currentUserId) {
return;
}

const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit =
detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;

// 现在用户输入的是 total 和 bad,需要算 available = total - bad
const totalQtyStr = isFirstSubmit ? firstQty : secondQty;
const badQtyStr = isFirstSubmit ? firstBadQty : secondBadQty;

if (!totalQtyStr || !badQtyStr) {
onSnackbar(
isFirstSubmit
? t("Please enter QTY and Bad QTY")
: t("Please enter Second QTY and Bad QTY"),
"error"
);
return;
}

const totalQty = parseFloat(totalQtyStr);
const badQty = parseFloat(badQtyStr);

if (Number.isNaN(totalQty) || Number.isNaN(badQty)) {
onSnackbar(t("Invalid QTY or Bad QTY"), "error");
return;
}

const availableQty = totalQty - badQty;

if (availableQty < 0) {
onSnackbar(t("Available QTY cannot be negative"), "error");
return;
}

setSaving(true);
try {
const request: SaveStockTakeRecordRequest = {
stockTakeRecordId: detail.stockTakeRecordId || null,
inventoryLotLineId: detail.id,
qty: availableQty, // 保存 available qty
badQty: badQty, // 保存 bad qty
remark: isSecondSubmit ? (remark || null) : null,
};
console.log("handleSaveStockTake: request:", request);
console.log("handleSaveStockTake: selectedSession.stockTakeId:", selectedSession.stockTakeId);
console.log("handleSaveStockTake: currentUserId:", currentUserId);

await saveStockTakeRecord(request, selectedSession.stockTakeId, currentUserId);

onSnackbar(t("Stock take record saved successfully"), "success");
handleCancelEdit();

const details = await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
);
setInventoryLotDetails(Array.isArray(details) ? details : []);
} catch (e: any) {
console.error("Save stock take record error:", e);
let errorMessage = t("Failed to save stock take record");

if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// ignore
}
}

onSnackbar(errorMessage, "error");
} finally {
setSaving(false);
}
onSnackbar(errorMessage, "error");
} finally {
setSaving(false);
}
}, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]);
},
[
selectedSession,
firstQty,
secondQty,
firstBadQty,
secondBadQty,
remark,
handleCancelEdit,
t,
currentUserId,
onSnackbar,
]
);

const handleBatchSubmitAll = useCallback(async () => {
if (!selectedSession || !currentUserId) {
console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId');
return;
}
const handleBatchSubmitAll = useCallback(
async () => {
if (!selectedSession || !currentUserId) {
console.log("handleBatchSubmitAll: Missing selectedSession or currentUserId");
return;
}

console.log("handleBatchSubmitAll: Starting batch save...");
setBatchSaving(true);
try {
const request: BatchSaveStockTakeRecordRequest = {
stockTakeId: selectedSession.stockTakeId,
stockTakeSection: selectedSession.stockTakeSession,
stockTakerId: currentUserId,
};

const result = await batchSaveStockTakeRecords(request);
console.log("handleBatchSubmitAll: Result:", result);

onSnackbar(
t("Batch save completed: {{success}} success, {{errors}} errors", {
success: result.successCount,
errors: result.errorCount,
}),
result.errorCount > 0 ? "warning" : "success"
);

console.log('handleBatchSubmitAll: Starting batch save...');
setBatchSaving(true);
try {
const request: BatchSaveStockTakeRecordRequest = {
stockTakeId: selectedSession.stockTakeId,
stockTakeSection: selectedSession.stockTakeSession,
stockTakerId: currentUserId,
};

const result = await batchSaveStockTakeRecords(request);
console.log('handleBatchSubmitAll: Result:', result);

onSnackbar(
t("Batch save completed: {{success}} success, {{errors}} errors", {
success: result.successCount,
errors: result.errorCount,
}),
result.errorCount > 0 ? "warning" : "success"
);

const details = await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
);
setInventoryLotDetails(Array.isArray(details) ? details : []);
} catch (e: any) {
console.error("handleBatchSubmitAll: Error:", e);
let errorMessage = t("Failed to batch save stock take records");
if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// ignore
const details = await getInventoryLotDetailsBySection(
selectedSession.stockTakeSession,
selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null
);
setInventoryLotDetails(Array.isArray(details) ? details : []);
} catch (e: any) {
console.error("handleBatchSubmitAll: Error:", e);
let errorMessage = t("Failed to batch save stock take records");

if (e?.message) {
errorMessage = e.message;
} else if (e?.response) {
try {
const errorData = await e.response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// ignore
}
}

onSnackbar(errorMessage, "error");
} finally {
setBatchSaving(false);
}
onSnackbar(errorMessage, "error");
} finally {
setBatchSaving(false);
}
}, [selectedSession, t, currentUserId, onSnackbar]);
},
[selectedSession, t, currentUserId, onSnackbar]
);

useEffect(() => {
handleBatchSubmitAllRef.current = handleBatchSubmitAll;
@@ -227,11 +278,12 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (target && (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
)) {
if (
target &&
(target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable)
) {
return;
}

@@ -240,48 +292,48 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
}

if (e.key.length === 1) {
setShortcutInput(prev => {
setShortcutInput((prev) => {
const newInput = prev + e.key;
if (newInput === '{2fitestall}') {
console.log('✅ Shortcut {2fitestall} detected!');
if (newInput === "{2fitestall}") {
console.log("✅ Shortcut {2fitestall} detected!");
setTimeout(() => {
if (handleBatchSubmitAllRef.current) {
console.log('Calling handleBatchSubmitAll...');
handleBatchSubmitAllRef.current().catch(err => {
console.error('Error in handleBatchSubmitAll:', err);
console.log("Calling handleBatchSubmitAll...");
handleBatchSubmitAllRef.current().catch((err) => {
console.error("Error in handleBatchSubmitAll:", err);
});
} else {
console.error('handleBatchSubmitAllRef.current is null');
console.error("handleBatchSubmitAllRef.current is null");
}
}, 0);
return "";
}
if (newInput.length > 15) {
return "";
}
if (newInput.length > 0 && !newInput.startsWith('{')) {
if (newInput.length > 0 && !newInput.startsWith("{")) {
return "";
}
if (newInput.length > 5 && !newInput.startsWith('{2fi')) {
if (newInput.length > 5 && !newInput.startsWith("{2fi")) {
return "";
}
return newInput;
});
} else if (e.key === 'Backspace') {
setShortcutInput(prev => prev.slice(0, -1));
} else if (e.key === 'Escape') {
} else if (e.key === "Backspace") {
setShortcutInput((prev) => prev.slice(0, -1));
} else if (e.key === "Escape") {
setShortcutInput("");
}
};

window.addEventListener('keydown', handleKeyPress);
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
window.removeEventListener("keydown", handleKeyPress);
};
}, []);

@@ -292,19 +344,46 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
return false;
}, []);

const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
.map((detail) => detail.warehouse)
.filter((warehouse) => warehouse && warehouse.trim() !== "")
)
).join(", ");

return (
<Box>
<Button onClick={onBack} sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}>
<Button
onClick={onBack}
sx={{ mb: 2, border: "1px solid", borderColor: "primary.main" }}
>
{t("Back to List")}
</Button>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>
{/* 如果需要显示快捷键输入,可以把这块注释打开 */}
{/*
{shortcutInput && (
<Box sx={{ mb: 2, p: 1.5, bgcolor: 'info.light', borderRadius: 1, border: '1px solid', borderColor: 'info.main' }}>
<Box
sx={{
mb: 2,
p: 1.5,
bgcolor: "info.light",
borderRadius: 1,
border: "1px solid",
borderColor: "info.main",
}}
>
<Typography variant="body2" color="info.dark" fontWeight={500}>
{t("Shortcut Input")}: <strong style={{ fontFamily: 'monospace', fontSize: '1.1em' }}>{shortcutInput}</strong>
{t("Shortcut Input")}:{" "}
<strong style={{ fontFamily: "monospace", fontSize: "1.1em" }}>
{shortcutInput}
</strong>
</Typography>
</Box>
)}
@@ -319,17 +398,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item")}</TableCell>
{/*<TableCell>{t("Item Name")}</TableCell>*/}
{/*<TableCell>{t("Lot No")}</TableCell>*/}
<TableCell>{t("Expiry Date")}</TableCell>
<TableCell>{t("Qty")}</TableCell>
<TableCell>{t("Bad Qty")}</TableCell>
{/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Status")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
@@ -337,7 +409,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={12} align="center">
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
@@ -347,152 +419,215 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
inventoryLotDetails.map((detail) => {
const isEditing = editingRecord?.id === detail.id;
const submitDisabled = isSubmitDisabled(detail);
const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty;
const isFirstSubmit =
!detail.stockTakeRecordId || !detail.firstStockTakeQty;
const isSecondSubmit =
detail.stockTakeRecordId &&
detail.firstStockTakeQty &&
!detail.secondStockTakeQty;

return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseCode || "-"}</TableCell>
<TableCell sx={{
maxWidth: 100,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>{detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""}</TableCell>
{/*
<TableCell
sx={{
maxWidth: 200,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}
>
{detail.itemName || "-"}
</TableCell>*/}
{/*<TableCell>{detail.lotNo || "-"}</TableCell>*/}
<TableCell>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</TableCell>
<TableCell>
<TableCell
sx={{
maxWidth: 150,
wordBreak: "break-word",
whiteSpace: "normal",
lineHeight: 1.5,
}}
>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstStockTakeQty ? (
<Typography variant="body2">
{t("First")}: {detail.firstStockTakeQty.toFixed(2)}
</Typography>
) : null}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondStockTakeQty ? (
<Typography variant="body2">
{t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
<Box>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>

{/* Qty + Bad Qty 合并显示/输入 */}
<TableCell sx={{ minWidth: 300 }}>
<Stack spacing={1}>
{/* First */}
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(firstQty || "0") -
parseFloat(firstBadQty || "0")
)}
</Typography>
</Stack>
) : detail.firstStockTakeQty != null ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
{t("First")}:{" "}
{formatNumber(
(detail.firstStockTakeQty ?? 0) +
(detail.firstBadQty ?? 0)
)}{" "}
(
{formatNumber(
detail.firstBadQty ?? 0
)}
) ={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography>
) : null}

{/* Second */}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(secondQty || "0") -
parseFloat(secondBadQty || "0")
)}
</Typography>
</Stack>
) : detail.secondStockTakeQty != null ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
{t("Second")}:{" "}
{formatNumber(
(detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0)
)}{" "}
(
{formatNumber(
detail.secondBadQty ?? 0
)}
) ={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}

{!detail.firstStockTakeQty &&
!detail.secondStockTakeQty &&
!isEditing && (
<Typography
variant="body2"
color="text.secondary"
>
-
</Typography>
)}
</Stack>
</TableCell>

{/* Remark */}
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
// If you want a single-line input, remove multiline/rows:
// multiline
// rows={2}
/>
</>
) : (
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell>
{detail.status ? (
<Chip size="small" label={t(detail.status)} color="default" />
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
/>
</>
) : (
"-"
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>

<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
<Chip
size="small"
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
)}
</TableCell>

<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
@@ -504,13 +639,9 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
>
{t("Save")}
</Button>
<Button
size="small"
onClick={handleCancelEdit}
>
<Button size="small" onClick={handleCancelEdit}>
{t("Cancel")}
</Button>
</Stack>
) : (
<Button
@@ -519,10 +650,10 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
)}


+ 2
- 2
src/components/StockTakeManagement/StockTakeManagementWrapper.tsx Dosyayı Görüntüle

@@ -1,13 +1,13 @@
import React from "react";
import GeneralLoading from "../General/GeneralLoading";
import StockTakeManagement from "./StockTakeManagement";
import StockTakeTabs from "./StockTakeTab";
interface SubComponents {
Loading: typeof GeneralLoading;
}

const StockTakeManagementWrapper: React.FC & SubComponents = async () => {
return <StockTakeManagement />;
return <StockTakeTabs/>;
};

StockTakeManagementWrapper.Loading = GeneralLoading;


+ 10
- 1
src/i18n/zh/common.json Dosyayı Görüntüle

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


+ 32
- 3
src/i18n/zh/inventory.json Dosyayı Görüntüle

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

}

+ 16
- 2
src/i18n/zh/jo.json Dosyayı Görüntüle

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

Yükleniyor…
İptal
Kaydet