ソースを参照

update expiry lot handle in jo/do and show qty will submit and no partly compelte by fronetend

MergeProblem1
CANCERYS\kw093 2日前
コミット
e79b060f32
4個のファイルの変更413行の追加139行の削除
  1. +185
    -47
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  2. +225
    -92
      src/components/Jodetail/newJobPickExecution.tsx
  3. +2
    -0
      src/i18n/zh/jo.json
  4. +1
    -0
      src/i18n/zh/pickOrder.json

+ 185
- 47
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx ファイルの表示

@@ -516,6 +516,42 @@ const ManualLotConfirmationModal: React.FC<{
</Modal>
);
};

/** 過期批號(未換有效批前):與 noLot 類似——單筆/批量預設提交量為 0,除非 Issue 改數 */
function isLotAvailabilityExpired(lot: any): boolean {
return String(lot?.lotAvailability || "").toLowerCase() === "expired";
}

/** Issue「改數」未寫入 SOL,刷新/換頁後需靠 session 還原,否則 Qty will submit 會回到 req */
const FG_ISSUE_PICKED_KEY = (doPickOrderId: number) =>
`fpsms-fg-issuePickedQty:${doPickOrderId}`;

function loadIssuePickedMap(doPickOrderId: number): Record<number, number> {
if (typeof window === "undefined" || !doPickOrderId) return {};
try {
const raw = sessionStorage.getItem(FG_ISSUE_PICKED_KEY(doPickOrderId));
if (!raw) return {};
const parsed = JSON.parse(raw) as Record<string, number>;
const out: Record<number, number> = {};
Object.entries(parsed).forEach(([k, v]) => {
const n = Number(v);
if (!Number.isNaN(n)) out[Number(k)] = n;
});
return out;
} catch {
return {};
}
}

function saveIssuePickedMap(doPickOrderId: number, map: Record<number, number>) {
if (typeof window === "undefined" || !doPickOrderId) return;
try {
sessionStorage.setItem(FG_ISSUE_PICKED_KEY(doPickOrderId), JSON.stringify(map));
} catch {
// quota / private mode
}
}

const PickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount }) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
@@ -621,15 +657,18 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
if (combinedLotData.length === 0) {
return { completed: 0, total: 0 };
}
const nonPendingCount = combinedLotData.filter(lot => {

// 與 allItemsReady 一致:noLot / 過期批號 的 pending 也算「已面對該行」可收尾
const nonPendingCount = combinedLotData.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase();
return status !== 'pending';
if (status !== "pending") return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true;
return false;
}).length;
return {
completed: nonPendingCount,
total: combinedLotData.length
total: combinedLotData.length,
};
}, [combinedLotData]);

@@ -789,6 +828,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
setIssuePickedQtyBySolId({});
return;
}
@@ -802,6 +842,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
setIssuePickedQtyBySolId({});
return;
}
@@ -1000,12 +1041,17 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
const doPid = hierarchicalData.fgInfo?.doPickOrderId;
if (doPid) {
setIssuePickedQtyBySolId(loadIssuePickedMap(doPid));
}
checkAllLotsCompleted(flatLotData);
} catch (error) {
console.error(" Error fetching combined lot data:", error);
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
setIssuePickedQtyBySolId({});
} finally {
setCombinedDataLoading(false);
}
@@ -2422,7 +2468,29 @@ useEffect(() => {
console.error("Error checking pick order completion:", error);
}
}, [currentUserId]);

const resolveSingleSubmitQty = useCallback(
(lot: any) => {
const required = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
const solId = Number(lot.stockOutLineId) || 0;
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) {
return Number(issuePicked);
}
const fromPick = pickQtyData[lotKey];
if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) {
return Number(fromPick);
}
if (lot.noLot === true) {
return 0;
}
if (isLotAvailabilityExpired(lot)) {
return 0;
}
return required;
},
[issuePickedQtyBySolId, pickQtyData]
);
// Handle reject lot
// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
@@ -2461,7 +2529,12 @@ useEffect(() => {
const solId = Number(issueData.stockOutLineId || issueData.stockOutLineId === 0 ? issueData.stockOutLineId : data?.stockOutLineId);
if (solId > 0) {
const picked = Number(issueData.actualPickQty || 0);
setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked }));
setIssuePickedQtyBySolId((prev) => {
const next = { ...prev, [solId]: picked };
const doId = fgPickOrders[0]?.doPickOrderId;
if (doId) saveIssuePickedMap(doId, next);
return next;
});
setCombinedLotData(prev => prev.map(lot => {
if (Number(lot.stockOutLineId) === solId) {
return { ...lot, actualPickQty: picked, stockOutLineQty: picked };
@@ -2488,7 +2561,7 @@ useEffect(() => {
} catch (error) {
console.error("Error submitting pick execution form:", error);
}
}, [fetchAllCombinedLotData, session]);
}, [fetchAllCombinedLotData, session, fgPickOrders]);

// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
@@ -2593,7 +2666,8 @@ const allItemsReady = useMemo(() => {
const isPending = status === 'pending';

// ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
if (lot.noLot === true) {
// ✅ 過期批號(未換批):與 noLot 相同,視為可收尾
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
return isChecked || isCompleted || isRejected || isPending;
}

@@ -2601,7 +2675,7 @@ const allItemsReady = useMemo(() => {
return isChecked || isCompleted || isRejected;
});
}, [combinedLotData]);
const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number, source: 'justComplete' | 'singleSubmit') => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
return;
@@ -2615,7 +2689,42 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
try {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
// Just Complete: mark checked only, real posting happens in batch submit
if (submitQty === 0) {
if (submitQty === 0 && source === 'justComplete') {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
console.log(`Setting status to 'checked' with qty: 0`);
const updateResult = await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'checked',
qty: 0
});
console.log('Update result:', updateResult);
const r: any = updateResult as any;
const updateOk =
r?.code === 'SUCCESS' ||
r?.type === 'completed' ||
typeof r?.id === 'number' ||
typeof r?.entity?.id === 'number' ||
(r?.message && r.message.includes('successfully'));
if (!updateResult || !updateOk) {
console.error('Failed to update stock out line status:', updateResult);
throw new Error('Failed to update stock out line status');
}
applyLocalStockOutLineUpdate(Number(lot.stockOutLineId), "checked", Number(lot.actualPickQty || 0));
void fetchAllCombinedLotData();
console.log("Just Complete marked as checked successfully (waiting for batch submit).");
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
return;
}
if (submitQty === 0 && source === 'singleSubmit') {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
@@ -2650,7 +2759,6 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return;
}
// FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
@@ -2661,7 +2769,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
if (cumulativeQty >= lot.requiredQty) {
newStatus = 'completed';
} else if (cumulativeQty > 0) {
newStatus = 'partially_completed';
newStatus = 'completed';
} else {
newStatus = 'checked'; // QR scanned but no quantity submitted yet
}
@@ -2728,7 +2836,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
const handleSkip = useCallback(async (lot: any) => {
try {
console.log("Just Complete clicked, mark checked with 0 qty for lot:", lot.lotNo);
await handleSubmitPickQtyWithQty(lot, 0);
await handleSubmitPickQtyWithQty(lot, 0, 'justComplete');
} catch (err) {
console.error("Error in Skip:", err);
}
@@ -2955,18 +3063,20 @@ const handleSubmitAllScanned = useCallback(async () => {
if (statusLower === "completed" || statusLower === "complete") {
return false;
}
// ✅ noLot 情况:允许 checked / pending / partially_completed / PARTIALLY_COMPLETE
if (lot.noLot === true) {
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
// ✅ noLot / 過期批號:與 noLot 相同,允許 pending(未換批也可批量收尾)
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
return (
status === "checked" ||
status === "pending" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
}
// ✅ 正常 lot:也放宽为允许 checked / pending / partially_completed / PARTIALLY_COMPLETE
// 这样即使用户先改数(状态变为 pending / partially_completed),仍然可以批量提交
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
return (
status === "checked" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
});
if (scannedLots.length === 0) {
@@ -2993,29 +3103,33 @@ const handleSubmitAllScanned = useCallback(async () => {
// 🔹 判断是否走“只改状态模式”
// 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成,
// 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。
const onlyComplete = lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;
// lot.stockOutLineStatus === "partially_completed" && false === true;
const onlyComplete =
lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;

const expired = isLotAvailabilityExpired(lot);

let targetActual: number;
let newStatus: string;
if (onlyComplete) {
// ✅ 只改状态:目标数量 = 当前数量,不再补拣

// ✅ 過期且未在 Issue 填實際量:與 noLot 一樣按 0 完成
if (expired && issuePicked === undefined) {
targetActual = 0;
newStatus = "completed";
} else if (onlyComplete) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else {
// ✅ 补拣模式:把剩余全部拣完
const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
const cumulativeQty = currentActualPickQty + remainingQty;
targetActual = cumulativeQty;
newStatus = "partially_completed";
if (requiredQty > 0 && cumulativeQty >= requiredQty) {
newStatus = "completed";
}
}
return {
stockOutLineId: Number(lot.stockOutLineId) || 0,
pickOrderLineId: Number(lot.pickOrderLineId),
@@ -3091,16 +3205,19 @@ const handleSubmitAllScanned = useCallback(async () => {
return false;
}
// ✅ 与 handleSubmitAllScanned 完全保持一致
if (lot.noLot === true) {
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
if (lot.noLot === true || isLotAvailabilityExpired(lot)) {
return (
status === "checked" ||
status === "pending" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
}
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
return (
status === "checked" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
});
// 添加调试日志
@@ -3369,6 +3486,7 @@ const handleSubmitAllScanned = useCallback(async () => {
<TableCell>{t("Lot#")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell align="center">{t("Scan Result")}</TableCell>
<TableCell align="center">{t("Qty will submit")}</TableCell>
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
</TableRow>
</TableHead>
@@ -3468,7 +3586,26 @@ paginatedData.map((lot, index) => {
</Box>
);
}

// 過期批號:與 noLot 同類——視為已掃到/可處理(含 pending),顯示警示色勾選
if (isLotAvailabilityExpired(lot) && status !== "rejected") {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: "warning.main",
"&.Mui-checked": { color: "warning.main" },
transform: "scale(1.3)",
}}
/>
</Box>
);
}

// 正常 lot:已扫描(checked/partially_completed/completed)
if (!isNoLot && status !== 'pending' && status !== 'rejected') {
return (
@@ -3510,6 +3647,7 @@ paginatedData.map((lot, index) => {
return null;
})()}
</TableCell>
<TableCell align="center">{resolveSingleSubmitQty(lot)}</TableCell>
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{(() => {
@@ -3568,9 +3706,9 @@ paginatedData.map((lot, index) => {
variant="contained"
onClick={() => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
const submitQty = resolveSingleSubmitQty(lot);
handlePickQtyChange(lotKey, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty, 'singleSubmit');
}}
disabled={
lot.lotAvailability === 'expired' ||


+ 225
- 92
src/components/Jodetail/newJobPickExecution.tsx ファイルの表示

@@ -71,6 +71,40 @@ interface Props {
onBackToList?: () => void;
}

/** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */
function isLotAvailabilityExpired(lot: any): boolean {
return String(lot?.lotAvailability || "").toLowerCase() === "expired";
}

const JO_ISSUE_PICKED_KEY = (pickOrderId: number) =>
`fpsms-jo-issuePickedQty:${pickOrderId}`;

function loadIssuePickedMapJo(pickOrderId: number): Record<number, number> {
if (typeof window === "undefined" || !pickOrderId) return {};
try {
const raw = sessionStorage.getItem(JO_ISSUE_PICKED_KEY(pickOrderId));
if (!raw) return {};
const parsed = JSON.parse(raw) as Record<string, number>;
const out: Record<number, number> = {};
Object.entries(parsed).forEach(([k, v]) => {
const n = Number(v);
if (!Number.isNaN(n)) out[Number(k)] = n;
});
return out;
} catch {
return {};
}
}

function saveIssuePickedMapJo(pickOrderId: number, map: Record<number, number>) {
if (typeof window === "undefined" || !pickOrderId) return;
try {
sessionStorage.setItem(JO_ISSUE_PICKED_KEY(pickOrderId), JSON.stringify(map));
} catch {
// ignore quota / private mode
}
}

// Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
const ManualLotConfirmationModal: React.FC<{
open: boolean;
@@ -481,6 +515,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const formProps = useForm();
const errors = formProps.formState.errors;
const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');

// Add QR modal states
const [qrModalOpen, setQrModalOpen] = useState(false);
@@ -825,6 +862,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
if (!pickOrderId) {
console.warn("⚠️ No pickOrderId provided, skipping API call");
setJobOrderData(null);
setIssuePickedQtyBySolId({});
return;
}
@@ -833,14 +871,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("✅ Job Order data (hierarchical):", jobOrderData);
setJobOrderData(jobOrderData);
setIssuePickedQtyBySolId(loadIssuePickedMapJo(pickOrderId));
// 使用辅助函数获取所有 lots(不再扁平化)
const allLots = getAllLotsFromHierarchical(jobOrderData);
getAllLotsFromHierarchical(jobOrderData);
} catch (error) {
console.error("❌ Error fetching job order data:", error);
setJobOrderData(null);
setIssuePickedQtyBySolId({});
} finally {
setCombinedDataLoading(false);
}
@@ -1703,9 +1743,30 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}));
}, []);

const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
/** 单笔「提交」数量:Issue 改数 → pickQtyData → noLot/過期 → 0 → 否则 required(對齊 GoodPickExecutiondetail) */
const resolveSingleSubmitQty = useCallback(
(lot: any) => {
const required = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
const solId = Number(lot.stockOutLineId) || 0;
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
if (issuePicked !== undefined && !Number.isNaN(Number(issuePicked))) {
return Number(issuePicked);
}
const fromPick = pickQtyData[lotKey];
if (fromPick !== undefined && fromPick !== null && !Number.isNaN(Number(fromPick))) {
return Number(fromPick);
}
if (lot.noLot === true || !lot.lotId) {
return 0;
}
if (isLotAvailabilityExpired(lot)) {
return 0;
}
return required;
},
[issuePickedQtyBySolId, pickQtyData]
);

const checkAndAutoAssignNext = useCallback(async () => {
if (!currentUserId) return;
@@ -1847,7 +1908,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// 记录该 SOL 的“目标实际拣货量=0”,让 batch submit 走 onlyComplete(不补拣到 required)
if (solId > 0) {
setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: 0 }));
setIssuePickedQtyBySolId((prev) => {
const next = { ...prev, [solId]: 0 };
const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
if (pid) saveIssuePickedMapJo(pid, next);
return next;
});
setLocalSolStatusById(prev => ({ ...prev, [solId]: 'checked' }));
}
@@ -1866,15 +1932,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
// 短拣一次 completed(對齊 GoodPickExecutiondetail)
let newStatus = "partially_completed";
if (cumulativeQty >= lot.requiredQty) {
newStatus = 'completed';
newStatus = "completed";
} else if (cumulativeQty > 0) {
newStatus = 'partially_completed';
newStatus = "completed";
} else {
newStatus = 'checked'; // QR scanned but no quantity submitted yet
newStatus = "checked";
}
console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
@@ -1892,7 +1957,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
qty: cumulativeQty
});
if (solId > 0) {
setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: cumulativeQty }));
setIssuePickedQtyBySolId((prev) => {
const next = { ...prev, [solId]: cumulativeQty };
const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
if (pid) saveIssuePickedMapJo(pid, next);
return next;
});
setLocalSolStatusById(prev => ({ ...prev, [solId]: newStatus }));
}
@@ -1966,29 +2036,25 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return () => window.removeEventListener("beforeunload", handler);
}, [hasPendingBatchSubmit]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot => {
const scannedLots = combinedLotData.filter((lot) => {
const status = lot.stockOutLineStatus;
const statusLower = String(status || "").toLowerCase();
if (statusLower === "completed" || statusLower === "complete") {
return false;
}
console.log("lot.noLot:", lot.noLot);
console.log("lot.status:", lot.stockOutLineStatus);
// ✅ no-lot:允許 pending / checked / partially_completed / PARTIALLY_COMPLETE
if (lot.noLot === true || !lot.lotId) {
// noLot / 過期批號:允許 pending(對齊 GoodPickExecutiondetail)
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) {
return (
status === 'checked' ||
status === 'pending' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
status === "checked" ||
status === "pending" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
}
// ✅ 有 lot:維持原本規則
return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
status === "checked" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
});
if (scannedLots.length === 0) {
@@ -2016,44 +2082,43 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`);
}

// ✅ 转换为 batchSubmitList 所需的格式
const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
const requiredQty = Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
const solId = Number(lot.stockOutLineId) || 0;
const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0);
const isNoLot = lot.noLot === true || !lot.lotId;
// ✅ 只改狀態模式:有 issuePicked 或 noLot
const onlyComplete =
lot.stockOutLineStatus === 'partially_completed' ||
issuePicked !== undefined ||
isNoLot;
let targetActual: number;
let newStatus: string;
if (onlyComplete) {
targetActual = currentActualPickQty; // no‑lot = 0,一律只改狀態
newStatus = 'completed';
} else {
const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
targetActual = currentActualPickQty + remainingQty;
newStatus =
requiredQty > 0 && targetActual >= requiredQty
? 'completed'
: 'partially_completed';
}
const solId = Number(lot.stockOutLineId) || 0;
const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0);
const onlyComplete =
lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;
const expired = isLotAvailabilityExpired(lot);
let targetActual: number;
let newStatus: string;
if (expired && issuePicked === undefined) {
targetActual = 0;
newStatus = "completed";
} else if (onlyComplete) {
targetActual = currentActualPickQty;
newStatus = "completed";
} else {
const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
const cumulativeQty = currentActualPickQty + remainingQty;
targetActual = cumulativeQty;
newStatus = "partially_completed";
if (requiredQty > 0 && cumulativeQty >= requiredQty) {
newStatus = "completed";
}
}
return {
stockOutLineId: Number(lot.stockOutLineId) || 0,
pickOrderLineId: Number(lot.pickOrderLineId),
inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
requiredQty,
actualPickQty: Number(targetActual),
actualPickQty: targetActual,
stockOutLineStatus: newStatus,
pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
noLot: Boolean(lot.noLot === true)
pickOrderConsoCode: String(lot.pickOrderConsoCode || ""),
noLot: Boolean(lot.noLot === true),
};
});
@@ -2118,29 +2183,24 @@ if (onlyComplete) {
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList, updateHandledBy, issuePickedQtyBySolId])
const scannedItemsCount = useMemo(() => {
return combinedLotData.filter(lot => {
return combinedLotData.filter((lot) => {
const status = lot.stockOutLineStatus;
const statusLower = String(status || "").toLowerCase();
if (statusLower === "completed" || statusLower === "complete") {
return false;
}
const isNoLot = lot.noLot === true || !lot.lotId;
if (isNoLot) {
// no-lot:pending / checked / partially_completed 都算「已掃描」
if (lot.noLot === true || !lot.lotId || isLotAvailabilityExpired(lot)) {
return (
status === 'pending' ||
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
status === "checked" ||
status === "pending" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
}
// 有 lot:維持原規則
return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
status === "checked" ||
status === "partially_completed" ||
status === "PARTIALLY_COMPLETE"
);
}).length;
}, [combinedLotData]);
@@ -2160,13 +2220,16 @@ if (onlyComplete) {
return combinedLotData.filter(lot => extractFloor(lot) === selectedFloor);
}, [combinedLotData, selectedFloor]);
// Progress bar data - 现在可以正确引用 filteredByFloor
// 與批量篩選一致:noLot / 過期 的 pending 也算已處理(對齊 GoodPickExecutiondetail)
const progress = useMemo(() => {
const data = selectedFloor ? filteredByFloor : combinedLotData;
if (data.length === 0) return { completed: 0, total: 0 };
const nonPendingCount = data.filter(lot =>
lot.stockOutLineStatus?.toLowerCase() !== 'pending'
).length;
const nonPendingCount = data.filter((lot) => {
const status = lot.stockOutLineStatus?.toLowerCase();
if (status !== "pending") return true;
if (lot.noLot === true || isLotAvailabilityExpired(lot)) return true;
return false;
}).length;
return { completed: nonPendingCount, total: data.length };
}, [selectedFloor, filteredByFloor, combinedLotData]);
// Handle reject lot
@@ -2240,7 +2303,12 @@ if (onlyComplete) {
const solId = Number(issueData.stockOutLineId || data?.stockOutLineId);
if (solId > 0) {
const picked = Number(issueData.actualPickQty || 0);
setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked }));
setIssuePickedQtyBySolId((prev) => {
const next = { ...prev, [solId]: picked };
const pid = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
if (pid) saveIssuePickedMapJo(pid, next);
return next;
});
}
} else {
console.error("❌ Failed to record pick execution issue:", result);
@@ -2254,7 +2322,7 @@ if (onlyComplete) {
} catch (error) {
console.error("Error submitting pick execution form:", error);
}
}, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId]);
}, [fetchJobOrderData, currentUserId, selectedLotForExecutionForm, updateHandledBy, filterArgs?.pickOrderId, filterArgs]);

// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
@@ -2542,13 +2610,14 @@ const sortedData = [...sourceData].sort((a, b) => {
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell align="right">{t("Available Qty")}</TableCell>
<TableCell align="center">{t("Scan Result")}</TableCell>
<TableCell align="center">{t("Qty will submit")}</TableCell>
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
<TableCell colSpan={11} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
@@ -2580,10 +2649,34 @@ const sortedData = [...sourceData].sort((a, b) => {
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
<TableCell>
{lot.noLot === true || !lot.lotId
? t("Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.") // i18n key,下一步加文案
: (lot.lotNo || '-')}
</TableCell>
<Box>
<Typography
sx={{
color:
lot.lotAvailability === "expired"
? "warning.main"
: "inherit",
}}
>
{lot.lotNo ? (
lot.lotAvailability === "expired" ? (
<>
{lot.lotNo}{" "}
{t(
"is expired. Please check around have available QR code or not.",
)}
</>
) : (
lot.lotNo
)
) : (
t(
"Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.",
)
)}
</Typography>
</Box>
</TableCell>
<TableCell align="right">
{(() => {
const requiredQty = lot.requiredQty || 0;
@@ -2607,8 +2700,7 @@ const sortedData = [...sourceData].sort((a, b) => {
const status = lot.stockOutLineStatus?.toLowerCase();
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
const isNoLot = !lot.lotNo;
// ✅ rejected lot:显示红色勾选(已扫描但被拒绝)

if (isRejected && !isNoLot) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
@@ -2626,8 +2718,25 @@ const sortedData = [...sourceData].sort((a, b) => {
</Box>
);
}
// ✅ 正常 lot:已扫描(checked/partially_completed/completed)

if (isLotAvailabilityExpired(lot) && status !== "rejected") {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: "warning.main",
"&.Mui-checked": { color: "warning.main" },
transform: "scale(1.3)",
}}
/>
</Box>
);
}

if (!isNoLot && status !== 'pending' && status !== 'rejected') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
@@ -2645,11 +2754,31 @@ const sortedData = [...sourceData].sort((a, b) => {
</Box>
);
}

if (isNoLot && (status === 'partially_completed' || status === 'completed')) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'error.main',
'&.Mui-checked': { color: 'error.main' },
transform: 'scale(1.3)',
}}
/>
</Box>
);
}

return null;
})()}
</TableCell>

<TableCell align="center">{resolveSingleSubmitQty(lot)}</TableCell>

<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{(() => {
@@ -2689,7 +2818,7 @@ const sortedData = [...sourceData].sort((a, b) => {
}
try {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
const submitQty = resolveSingleSubmitQty(lot);
handlePickQtyChange(lotKey, submitQty);

await handleSubmitPickQtyWithQty(lot, submitQty);
@@ -2722,7 +2851,12 @@ const sortedData = [...sourceData].sort((a, b) => {
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
lot.stockOutLineStatus === 'completed' || lot.noLot === true || !lot.lotId
lot.lotAvailability === "expired" ||
lot.stockOutLineStatus === "completed" ||
lot.noLot === true ||
!lot.lotId ||
(Number(lot.stockOutLineId) > 0 &&
actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
sx={{
fontSize: '0.7rem',
@@ -2764,13 +2898,12 @@ const sortedData = [...sourceData].sort((a, b) => {
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'partially_completed' ||
lot.lotAvailability === 'expired' ||
lot.noLot === true ||
!lot.lotId ||
(Number(lot.stockOutLineId) > 0 &&
Object.prototype.hasOwnProperty.call(
issuePickedQtyBySolId,
Number(lot.stockOutLineId)
))
issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined)
}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
>


+ 2
- 0
src/i18n/zh/jo.json ファイルの表示

@@ -11,6 +11,7 @@
"Name": "成品/半成品名稱",
"Picked Qty": "已提料數量",
"Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有QR碼,可能是剛剛入庫或轉移入庫或轉移出庫。",
"is expired. Please check around have available QR code or not.": "已過期。請檢查周圍是否有可用的 QR 碼。",
"Confirm All": "確認所有提料",
"Wait Time [minutes]": "等待時間(分鐘)",
"Job Process Status Dashboard": "儀表板 - 工單狀態",
@@ -25,6 +26,7 @@
"UoM": "銷售單位",
"Select Another Bag Lot":"選擇另一個包裝袋",
"No": "沒有",
"Qty will submit": "提交數量",
"Packaging":"提料中",
"Overall Time Remaining": "總剩餘時間",
"User not found with staffNo:": "用戶不存在",


+ 1
- 0
src/i18n/zh/pickOrder.json ファイルの表示

@@ -33,6 +33,7 @@
"Pick Order Code(s)": "提料單編號",
"Delivery Order Code(s)": "提料單編號",
"Start Success": "開始成功",
"Qty will submit": "提交數量",
"Truck Lance Code": "車線號碼",
"Pick Order Codes": "提料單編號",
"Pick Order Lines": "提料單行數",


読み込み中…
キャンセル
保存