Преглед изворни кода

remove old lot confirm mdoel and not lot order

production
CANCERYS\kw093 пре 6 дана
родитељ
комит
2af6099dfb
2 измењених фајлова са 77 додато и 237 уклоњено
  1. +74
    -236
      src/components/JoWorkbench/newJobPickExecution.tsx
  2. +3
    -1
      src/i18n/zh/jo.json

+ 74
- 236
src/components/JoWorkbench/newJobPickExecution.tsx Прегледај датотеку

@@ -69,7 +69,6 @@ import { SessionWithTokens } from "@/config/authConfig";
import { fetchStockInLineInfo } from "@/app/api/po/actions"; import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm"; import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm";
import FGPickOrderCard from "../Jodetail/FGPickOrderCard"; import FGPickOrderCard from "../Jodetail/FGPickOrderCard";
import LotConfirmationModal from "../Jodetail/LotConfirmationModal";
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
import LinearProgressWithLabel from "../common/LinearProgressWithLabel"; import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
import ScanStatusAlert from "../common/ScanStatusAlert"; import ScanStatusAlert from "../common/ScanStatusAlert";
@@ -634,10 +633,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
stopScan, stopScan,
resetScan, resetScan,
} = useQrCodeScannerContext(); } = useQrCodeScannerContext();
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [lotConfirmationError, setLotConfirmationError] = useState<
string | null
>(null);
const [expectedLotData, setExpectedLotData] = useState<any>(null); const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null); const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false); const [isConfirmingLot, setIsConfirmingLot] = useState(false);
@@ -773,9 +768,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null ((latestQr: string, qrScanCountAtInvoke?: number) => Promise<void>) | null
>(null); >(null);
const resetScanRef = useRef<(() => void) | null>(null); const resetScanRef = useRef<(() => void) | null>(null);
const lotConfirmLastQrRef = useRef<string>("");
const lotConfirmSkipNextScanRef = useRef<boolean>(false);
const lotConfirmOpenedAtRef = useRef<number>(0);


// Manual lot confirmation modal state (test shortcut {2fic}) // Manual lot confirmation modal state (test shortcut {2fic})
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = const [manualLotConfirmationOpen, setManualLotConfirmationOpen] =
@@ -1323,10 +1315,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log(" Successfully assigned pick order"); console.log(" Successfully assigned pick order");
if (!assignSkippedByStatus) { if (!assignSkippedByStatus) {
try { try {
/*
const refreshed = const refreshed =
await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench( await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(
pickOrderId, pickOrderId,
); );
const uniqueItemIds = Array.from( const uniqueItemIds = Array.from(
new Set( new Set(
(refreshed?.pickOrderLines ?? []) (refreshed?.pickOrderLines ?? [])
@@ -1339,6 +1333,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
), ),
), ),
); );
await Promise.all( await Promise.all(
uniqueItemIds.map((itemId) => uniqueItemIds.map((itemId) =>
updateJoPickOrderHandledBy({ updateJoPickOrderHandledBy({
@@ -1348,6 +1343,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}), }),
), ),
); );
*/
} catch (handledBySyncError) { } catch (handledBySyncError) {
console.warn( console.warn(
"⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:", "⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:",
@@ -1587,72 +1583,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}, },
[combinedLotData, tPick], [combinedLotData, tPick],
); );
const handleLotMismatch = useCallback(
(expectedLot: any, scannedLot: any) => {
console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", {
expectedLot,
scannedLot,
});
console.log(
"⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms",
);

// ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
// ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
setTimeout(() => {
setExpectedLotData(expectedLot);
setScannedLotData({
...scannedLot,
lotNo: scannedLot.lotNo || null,
});
lotConfirmSkipNextScanRef.current = true;
lotConfirmOpenedAtRef.current = Date.now();
setLotConfirmationOpen(true);
console.log(
"⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation",
);
}, 0);

// ✅ Fetch lotNo in background for display purposes (cached)
// ✅ This is ONLY for display - it does NOT process any lot
if (!scannedLot.lotNo && scannedLot.stockInLineId) {
console.log(
`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`,
);
fetchStockInLineInfoCached(scannedLot.stockInLineId)
.then((info) => {
console.log(
`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`,
);
startTransition(() => {
setScannedLotData((prev: any) => ({
...prev,
lotNo: info.lotNo || null,
}));
});
})
.catch((error) => {
console.error(
`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`,
error,
);
// ignore display fetch errors - this does NOT affect processing
});
}
},
[fetchStockInLineInfoCached],
);


const clearLotConfirmationState = useCallback( const clearLotConfirmationState = useCallback(
(clearProcessedRefs: boolean = false) => { (clearProcessedRefs: boolean = false) => {
setLotConfirmationOpen(false);
setLotConfirmationError(null);
setExpectedLotData(null); setExpectedLotData(null);
setScannedLotData(null); setScannedLotData(null);
setSelectedLotForQr(null); setSelectedLotForQr(null);
lotConfirmLastQrRef.current = "";
lotConfirmSkipNextScanRef.current = false;
lotConfirmOpenedAtRef.current = 0;


if (clearProcessedRefs) { if (clearProcessedRefs) {
setTimeout(() => { setTimeout(() => {
@@ -1683,7 +1619,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr); console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);


setIsConfirmingLot(true); setIsConfirmingLot(true);
setLotConfirmationError(null);
try { try {
let newLotLineId = effectiveScannedLot?.inventoryLotLineId; let newLotLineId = effectiveScannedLot?.inventoryLotLineId;
if (!newLotLineId && effectiveScannedLot?.stockInLineId) { if (!newLotLineId && effectiveScannedLot?.stockInLineId) {
@@ -1787,7 +1722,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
tPick( tPick(
"Lot switch failed; pick line was not marked as checked.", "Lot switch failed; pick line was not marked as checked.",
); );
setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
@@ -1831,7 +1765,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
`换批失败:stockInLineId ${ `换批失败:stockInLineId ${
effectiveScannedLot?.stockInLineId ?? "" effectiveScannedLot?.stockInLineId ?? ""
} 不存在或无法匹配`; } 不存在或无法匹配`;
setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
@@ -1849,7 +1782,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const silId = effectiveScannedLot?.stockInLineId; const silId = effectiveScannedLot?.stockInLineId;
if (!lotNoToScan) { if (!lotNoToScan) {
const errMsg = tPick("Cannot resolve lot number for confirmation."); const errMsg = tPick("Cannot resolve lot number for confirmation.");
setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
@@ -1869,7 +1801,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const errMsg = const errMsg =
(res as { message?: string })?.message || (res as { message?: string })?.message ||
tPick("Workbench scan-pick failed."); tPick("Workbench scan-pick failed.");
setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
@@ -1942,7 +1873,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} catch (error) { } catch (error) {
console.error("Error confirming lot substitution:", error); console.error("Error confirming lot substitution:", error);
const errMsg = tPick("Lot confirmation failed. Please try again."); const errMsg = tPick("Lot confirmation failed. Please try again.");
setLotConfirmationError(errMsg);
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
setQrScanErrorMsg(errMsg); setQrScanErrorMsg(errMsg);
@@ -1969,84 +1899,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
], ],
); );


const handleLotConfirmationByRescan = useCallback(
async (rawQr: string): Promise<boolean> => {
if (
!lotConfirmationOpen ||
!selectedLotForQr ||
!expectedLotData ||
!scannedLotData
) {
return false;
}
let payload: any = null;
try {
payload = JSON.parse(rawQr);
} catch {
payload = null;
}
const expectedStockInLineId = Number(selectedLotForQr.stockInLineId);
const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId);
if (payload?.stockInLineId && payload?.itemId) {
const rescannedStockInLineId = Number(payload.stockInLineId);
if (
Number.isFinite(expectedStockInLineId) &&
rescannedStockInLineId === expectedStockInLineId
) {
clearLotConfirmationState(false);
if (processOutsideQrCodeRef.current) {
await processOutsideQrCodeRef.current(JSON.stringify(payload));
}
return true;
}
if (
Number.isFinite(mismatchedStockInLineId) &&
rescannedStockInLineId === mismatchedStockInLineId
) {
await handleLotConfirmation();
return true;
}
await handleLotConfirmation({
lotNo: null,
itemCode: expectedLotData?.itemCode,
itemName: expectedLotData?.itemName,
inventoryLotLineId: null,
stockInLineId: rescannedStockInLineId,
});
return true;
} else {
const scannedText = rawQr?.trim();
const expectedLotNo = expectedLotData?.lotNo?.trim();
const mismatchedLotNo = scannedLotData?.lotNo?.trim();
if (expectedLotNo && scannedText === expectedLotNo) {
clearLotConfirmationState(false);
if (processOutsideQrCodeRef.current) {
await processOutsideQrCodeRef.current(
JSON.stringify({
itemId: selectedLotForQr.itemId,
stockInLineId: selectedLotForQr.stockInLineId,
}),
);
}
return true;
}
if (mismatchedLotNo && scannedText === mismatchedLotNo) {
await handleLotConfirmation();
return true;
}
}
return false;
},
[
lotConfirmationOpen,
selectedLotForQr,
expectedLotData,
scannedLotData,
handleLotConfirmation,
clearLotConfirmationState,
],
);

const processOutsideQrCode = useCallback( const processOutsideQrCode = useCallback(
async (latestQr: string, _qrScanCountAtInvoke?: number) => { async (latestQr: string, _qrScanCountAtInvoke?: number) => {
const totalStartTime = performance.now(); const totalStartTime = performance.now();
@@ -2993,32 +2845,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return; return;
} }


if (lotConfirmationOpen) {
if (isConfirmingLot) return;
if (lotConfirmSkipNextScanRef.current) {
lotConfirmSkipNextScanRef.current = false;
lotConfirmLastQrRef.current = latestQr || "";
return;
}
if (!latestQr) return;
const sameQr = latestQr === lotConfirmLastQrRef.current;
const justOpened =
lotConfirmOpenedAtRef.current > 0 &&
Date.now() - lotConfirmOpenedAtRef.current < 800;
if (sameQr && justOpened) return;
lotConfirmLastQrRef.current = latestQr;
void (async () => {
try {
const handled = await handleLotConfirmationByRescan(latestQr);
if (handled && resetScanRef.current) {
resetScanRef.current();
}
} catch (e) {
console.error("Lot confirmation rescan failed:", e);
}
})();
return;
}
// Skip processing if manual modal open for same QR // Skip processing if manual modal open for same QR
if (manualLotConfirmationOpen) { if (manualLotConfirmationOpen) {
if (latestQr === lastProcessedQrRef.current) return; if (latestQr === lastProcessedQrRef.current) return;
@@ -3065,9 +2891,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
isManualScanning, isManualScanning,
isRefreshingData, isRefreshingData,
combinedLotData.length, combinedLotData.length,
lotConfirmationOpen,
manualLotConfirmationOpen, manualLotConfirmationOpen,
handleLotConfirmationByRescan,
isConfirmingLot, isConfirmingLot,
]); ]);


@@ -3850,13 +3674,40 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Pagination data: align DO workbench grouping display // Pagination data: align DO workbench grouping display
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
const sourceData = selectedFloor ? filteredByFloor : combinedLotData; const sourceData = selectedFloor ? filteredByFloor : combinedLotData;

/** 同 pick_order_line 內「無庫位 / no-lot」列 extractFloor 為空,若仍用 0 會被排到全表最後,編號欄也變成新群組。繼承該行最大樓層權重並以 polId 相鄰排序。 */
const lineMaxFloorOrder = new Map<number, number>();
for (const lot of sourceData) {
const polId = Number(lot.pickOrderLineId);
if (!Number.isFinite(polId) || polId <= 0) continue;
const o = floorSortOrder(extractFloor(lot));
const prev = lineMaxFloorOrder.get(polId) ?? 0;
if (o > prev) lineMaxFloorOrder.set(polId, o);
}
const effectiveFloorOrder = (lot: any): number => {
const own = floorSortOrder(extractFloor(lot));
if (own > 0) return own;
const polId = Number(lot.pickOrderLineId);
if (Number.isFinite(polId) && polId > 0) {
const inherited = lineMaxFloorOrder.get(polId);
if (inherited != null && inherited > 0) return inherited;
}
return 0;
};
const isNoLotTailRow = (lot: any) =>
lot.noLot === true || lot.lotId == null || lot.lotId === undefined;

const sortedData = [...sourceData].sort((a, b) => { const sortedData = [...sourceData].sort((a, b) => {
const floorA = extractFloor(a);
const floorB = extractFloor(b);
const orderA = floorSortOrder(floorA);
const orderB = floorSortOrder(floorB);
if (orderA !== orderB) return orderB - orderA; // 4F, 3F, 2F
// Same floor: group same item together first (DO-like visual grouping)
const efA = effectiveFloorOrder(a);
const efB = effectiveFloorOrder(b);
if (efA !== efB) return efB - efA; // 4F, 3F, 2F(含繼承樓層的缺口列)

const polA = Number(a.pickOrderLineId);
const polB = Number(b.pickOrderLineId);
const hasPolA = Number.isFinite(polA) && polA > 0;
const hasPolB = Number.isFinite(polB) && polB > 0;
if (hasPolA && hasPolB && polA !== polB) return polA - polB;

const aItem = String(a.itemCode || ""); const aItem = String(a.itemCode || "");
const bItem = String(b.itemCode || ""); const bItem = String(b.itemCode || "");
if (aItem !== bItem) return aItem.localeCompare(bItem); if (aItem !== bItem) return aItem.localeCompare(bItem);
@@ -3865,6 +3716,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const bName = String(b.itemName || ""); const bName = String(b.itemName || "");
if (aName !== bName) return aName.localeCompare(bName); if (aName !== bName) return aName.localeCompare(bName);


const tailA = isNoLotTailRow(a) ? 1 : 0;
const tailB = isNoLotTailRow(b) ? 1 : 0;
if (tailA !== tailB) return tailA - tailB;

const aIndex = Number(a.routerIndex ?? 0); const aIndex = Number(a.routerIndex ?? 0);
const bIndex = Number(b.routerIndex ?? 0); const bIndex = Number(b.routerIndex ?? 0);
if (aIndex !== bIndex) return aIndex - bIndex; if (aIndex !== bIndex) return aIndex - bIndex;
@@ -4715,20 +4570,18 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
</Stack> </Stack>


{/* QR Code Modal */} {/* QR Code Modal */}
{!lotConfirmationOpen && (
<QrCodeModal
open={qrModalOpen}
onClose={() => {
setQrModalOpen(false);
setSelectedLotForQr(null);
// Keep scanner active like GoodPickExecutiondetail.
resetScan();
}}
lot={selectedLotForQr}
combinedLotData={combinedLotData}
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>
)}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
setQrModalOpen(false);
setSelectedLotForQr(null);
// Keep scanner active like GoodPickExecutiondetail.
resetScan();
}}
lot={selectedLotForQr}
combinedLotData={combinedLotData}
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>
<WorkbenchLotLabelPrintModal <WorkbenchLotLabelPrintModal
open={workbenchLotLabelModalOpen} open={workbenchLotLabelModalOpen}
onClose={() => { onClose={() => {
@@ -4765,54 +4618,39 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange} onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange}
reminderText={workbenchLotLabelReminderText ?? undefined} reminderText={workbenchLotLabelReminderText ?? undefined}
/> />
{/* Add Lot Confirmation Modal */}
{lotConfirmationOpen && expectedLotData && scannedLotData && (
<LotConfirmationModal
open={lotConfirmationOpen}
onClose={() => {
console.log(
`⏱️ [LOT CONFIRM MODAL] Closing modal, reset scanner and release raw-QR dedupe`,
);
if (resetScanRef.current) {
resetScanRef.current();
}
clearLotConfirmationState(false);
setTimeout(() => {
lastProcessedQrRef.current = "";
processedQrCodesRef.current.clear();
}, 250);
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}
scannedLot={scannedLotData}
isLoading={isConfirmingLot}
errorMessage={lotConfirmationError}
/>
)}

{/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
{/* Manual Lot Confirmation Modal (test shortcut {2fic}); JO Workbench 不使用 LotConfirmationModal,直接走 handleLotConfirmation */}
<ManualLotConfirmationModal <ManualLotConfirmationModal
open={manualLotConfirmationOpen} open={manualLotConfirmationOpen}
onClose={() => setManualLotConfirmationOpen(false)} onClose={() => setManualLotConfirmationOpen(false)}
// Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
onConfirm={(currentLotNo, newLotNo) => { onConfirm={(currentLotNo, newLotNo) => {
// Use existing manual flow from handleManualLotConfirmation in other screens:
// Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
// For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
setManualLotConfirmationOpen(false);
const row = selectedLotForQr;
if (
!row?.stockOutLineId ||
!row?.pickOrderLineId ||
row.itemId == null
) {
alert(
t(
"Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.",
),
);
return;
}
setExpectedLotData({ setExpectedLotData({
...row,
lotNo: currentLotNo, lotNo: currentLotNo,
itemCode: "",
itemName: "",
}); });
setScannedLotData({ setScannedLotData({
lotNo: newLotNo, lotNo: newLotNo,
itemCode: "",
itemName: "",
itemCode: String(row.itemCode ?? ""),
itemName: String(row.itemName ?? ""),
inventoryLotLineId: null, inventoryLotLineId: null,
stockInLineId: null, stockInLineId: null,
}); });
setManualLotConfirmationOpen(false);
setLotConfirmationOpen(true);
window.setTimeout(() => {
void handleLotConfirmation();
}, 0);
}} }}
expectedLot={expectedLotData} expectedLot={expectedLotData}
scannedLot={scannedLotData} scannedLot={scannedLotData}


+ 3
- 1
src/i18n/zh/jo.json Прегледај датотеку

@@ -385,6 +385,7 @@
"Printed Successfully.": "成功列印", "Printed Successfully.": "成功列印",
"Submit All Scanned": "提交所有已掃描項目", "Submit All Scanned": "提交所有已掃描項目",
"Submitting...": "提交中...", "Submitting...": "提交中...",
"is unavable. Please check around have available QR code or not.": "此批號不可用,請檢查周圍是否有可用的 QR 碼。",
"COMPLETED": "已完成", "COMPLETED": "已完成",
"success": "成功", "success": "成功",
"Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量", "Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量",
@@ -605,5 +606,6 @@
"BOM Type": "BOM 類型", "BOM Type": "BOM 類型",
"BOM Description": "BOM 說明", "BOM Description": "BOM 說明",
"Floor": "樓層", "Floor": "樓層",
"Finish": "完成"
"Finish": "完成",
"Open 挑號 QR 碼 on a pick line first, then scan {2fic} to use manual lot substitution.": "請先點選一列並開啟「挑號 QR 碼」,再掃描 {2fic} 以使用手動換批。"
} }

Loading…
Откажи
Сачувај