Ver a proveniência

remove old lot confirm mdoel and not lot order

production
CANCERYS\kw093 há 6 dias
ascendente
cometimento
2af6099dfb
2 ficheiros alterados com 77 adições e 237 eliminações
  1. +74
    -236
      src/components/JoWorkbench/newJobPickExecution.tsx
  2. +3
    -1
      src/i18n/zh/jo.json

+ 74
- 236
src/components/JoWorkbench/newJobPickExecution.tsx Ver ficheiro

@@ -69,7 +69,6 @@ import { SessionWithTokens } from "@/config/authConfig";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "../Jodetail/JobPickExecutionForm";
import FGPickOrderCard from "../Jodetail/FGPickOrderCard";
import LotConfirmationModal from "../Jodetail/LotConfirmationModal";
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
import ScanStatusAlert from "../common/ScanStatusAlert";
@@ -634,10 +633,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
stopScan,
resetScan,
} = useQrCodeScannerContext();
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [lotConfirmationError, setLotConfirmationError] = useState<
string | null
>(null);
const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false);
@@ -773,9 +768,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
((latestQr: string, qrScanCountAtInvoke?: number) => Promise<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})
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] =
@@ -1323,10 +1315,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
console.log(" Successfully assigned pick order");
if (!assignSkippedByStatus) {
try {
/*
const refreshed =
await fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench(
pickOrderId,
);
const uniqueItemIds = Array.from(
new Set(
(refreshed?.pickOrderLines ?? [])
@@ -1339,6 +1333,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
),
),
);
await Promise.all(
uniqueItemIds.map((itemId) =>
updateJoPickOrderHandledBy({
@@ -1348,6 +1343,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
}),
),
);
*/
} catch (handledBySyncError) {
console.warn(
"⚠️ Assigned but failed to sync JoPickOrder.handledBy for some lines:",
@@ -1587,72 +1583,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
},
[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(
(clearProcessedRefs: boolean = false) => {
setLotConfirmationOpen(false);
setLotConfirmationError(null);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
lotConfirmLastQrRef.current = "";
lotConfirmSkipNextScanRef.current = false;
lotConfirmOpenedAtRef.current = 0;

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

setIsConfirmingLot(true);
setLotConfirmationError(null);
try {
let newLotLineId = effectiveScannedLot?.inventoryLotLineId;
if (!newLotLineId && effectiveScannedLot?.stockInLineId) {
@@ -1787,7 +1722,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
tPick(
"Lot switch failed; pick line was not marked as checked.",
);
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(errMsg);
@@ -1831,7 +1765,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
`换批失败:stockInLineId ${
effectiveScannedLot?.stockInLineId ?? ""
} 不存在或无法匹配`;
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(errMsg);
@@ -1849,7 +1782,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const silId = effectiveScannedLot?.stockInLineId;
if (!lotNoToScan) {
const errMsg = tPick("Cannot resolve lot number for confirmation.");
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(errMsg);
@@ -1869,7 +1801,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const errMsg =
(res as { message?: string })?.message ||
tPick("Workbench scan-pick failed.");
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(errMsg);
@@ -1942,7 +1873,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
} catch (error) {
console.error("Error confirming lot substitution:", error);
const errMsg = tPick("Lot confirmation failed. Please try again.");
setLotConfirmationError(errMsg);
setQrScanError(true);
setQrScanSuccess(false);
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(
async (latestQr: string, _qrScanCountAtInvoke?: number) => {
const totalStartTime = performance.now();
@@ -2993,32 +2845,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
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
if (manualLotConfirmationOpen) {
if (latestQr === lastProcessedQrRef.current) return;
@@ -3065,9 +2891,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
isManualScanning,
isRefreshingData,
combinedLotData.length,
lotConfirmationOpen,
manualLotConfirmationOpen,
handleLotConfirmationByRescan,
isConfirmingLot,
]);

@@ -3850,13 +3674,40 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
// Pagination data: align DO workbench grouping display
const paginatedData = useMemo(() => {
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 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 bItem = String(b.itemCode || "");
if (aItem !== bItem) return aItem.localeCompare(bItem);
@@ -3865,6 +3716,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const bName = String(b.itemName || "");
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 bIndex = Number(b.routerIndex ?? 0);
if (aIndex !== bIndex) return aIndex - bIndex;
@@ -4715,20 +4570,18 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
</Stack>

{/* 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
open={workbenchLotLabelModalOpen}
onClose={() => {
@@ -4765,54 +4618,39 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
onSubmitQtyChange={handleWorkbenchLotLabelSubmitQtyChange}
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
open={manualLotConfirmationOpen}
onClose={() => setManualLotConfirmationOpen(false)}
// Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
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({
...row,
lotNo: currentLotNo,
itemCode: "",
itemName: "",
});
setScannedLotData({
lotNo: newLotNo,
itemCode: "",
itemName: "",
itemCode: String(row.itemCode ?? ""),
itemName: String(row.itemName ?? ""),
inventoryLotLineId: null,
stockInLineId: null,
});
setManualLotConfirmationOpen(false);
setLotConfirmationOpen(true);
window.setTimeout(() => {
void handleLotConfirmation();
}, 0);
}}
expectedLot={expectedLotData}
scannedLot={scannedLotData}


+ 3
- 1
src/i18n/zh/jo.json Ver ficheiro

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

Carregando…
Cancelar
Guardar