Просмотр исходного кода

建議批號 UOM 檢核/不符則擋、標籤列印只顯示同 UOM

production
CANCERYS\kw093 8 часов назад
Родитель
Сommit
ff6789711c
13 измененных файлов: 581 добавлений и 271 удалений
  1. +11
    -2
      src/app/api/doworkbench/actions.ts
  2. +5
    -1
      src/app/api/inventory/actions.ts
  3. +24
    -0
      src/app/api/settings/item/actions.ts
  4. +103
    -11
      src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx
  5. +60
    -19
      src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx
  6. +1
    -0
      src/components/InventorySearch/InventorySearch.tsx
  7. +8
    -3
      src/components/PickOrderSearch/AssignAndRelease.tsx
  8. +12
    -13
      src/components/PickOrderSearch/CreatedItemsTable.tsx
  9. +55
    -70
      src/components/PickOrderSearch/SearchResultsTable.tsx
  10. +155
    -15
      src/components/PickOrderSearch/WorkbenchPickExecution.tsx
  11. +139
    -137
      src/components/PickOrderSearch/newcreatitem.tsx
  12. +4
    -0
      src/i18n/en/pickOrder.json
  13. +4
    -0
      src/i18n/zh/pickOrder.json

+ 11
- 2
src/app/api/doworkbench/actions.ts Просмотреть файл

@@ -417,6 +417,8 @@ export async function fetchWorkbenchCompletedLotDetails(
export type WorkbenchScanPayload = { export type WorkbenchScanPayload = {
itemId: number; itemId: number;
stockInLineId: number; stockInLineId: number;
/** POL UomConversion id — when set, sameItemLots only include matching lot UOM */
uomId?: number | null;
}; };
export async function fetchWorkbenchPrinters() { export async function fetchWorkbenchPrinters() {
return serverFetchJson<any[]>(`${BASE_API_URL}/printers`, { return serverFetchJson<any[]>(`${BASE_API_URL}/printers`, {
@@ -433,9 +435,16 @@ export async function analyzeWorkbenchQrCode(payload: WorkbenchScanPayload) {
}); });
} }


export async function fetchWorkbenchAvailableLotsByItem(itemId: number) {
export async function fetchWorkbenchAvailableLotsByItem(
itemId: number,
uomId?: number | null,
) {
const qs =
uomId != null && Number.isFinite(Number(uomId)) && Number(uomId) > 0
? `?uomId=${Number(uomId)}`
: "";
return serverFetchJson<any>( return serverFetchJson<any>(
`${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}`,
`${BASE_API_URL}/inventoryLotLine/workbench/available-lots-by-item/${itemId}${qs}`,
{ {
method: "GET", method: "GET",
cache: "no-store", cache: "no-store",


+ 5
- 1
src/app/api/inventory/actions.ts Просмотреть файл

@@ -23,6 +23,7 @@ export interface LotLineInfo {
lotNo: string; lotNo: string;
remainingQty: number; remainingQty: number;
uom: string; uom: string;
uomId: number;
} }


export interface SearchInventoryLotLine extends Pageable { export interface SearchInventoryLotLine extends Pageable {
@@ -170,7 +171,10 @@ async function fetchInventoriesLatestImpl(data: SearchInventory) {


export const fetchInventories = cache(fetchInventoriesImpl); export const fetchInventories = cache(fetchInventoriesImpl);


/** Inventory search page: latest inventory row per item (no baseUnit/uomId filter). */
/**
* FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17
* Inventory search page: latest inventory row per item (no baseUnit/uomId filter).
*/
export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl);


/** Bypass React cache() after mutations so lists show fresh qty. */ /** Bypass React cache() after mutations so lists show fresh qty. */


+ 24
- 0
src/app/api/settings/item/actions.ts Просмотреть файл

@@ -95,6 +95,30 @@ export interface ItemWithDetails {
uomDesc: string; uomDesc: string;
currentStockBalance: number; currentStockBalance: number;
} }

export interface PickUomOption {
uomId: number;
uomCode: string;
uomDesc: string;
}

export interface AvailablePickUomsResponse {
itemId: number;
stockUom: PickUomOption | null;
options: PickUomOption[];
}

/** FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 */
export const fetchAvailablePickUoms = cache(async (itemId: number) => {
return serverFetchJson<AvailablePickUomsResponse>(
`${BASE_API_URL}/items/${itemId}/available-pick-uoms`,
{
method: "GET",
next: { tags: ["items"] },
},
);
});

export const fetchItemsWithDetails = cache(async (searchParams?: Record<string, any>) => { export const fetchItemsWithDetails = cache(async (searchParams?: Record<string, any>) => {
if (searchParams) { if (searchParams) {
const queryString = new URLSearchParams(searchParams).toString(); const queryString = new URLSearchParams(searchParams).toString();


+ 103
- 11
src/components/DoWorkbench/WorkbenchGoodPickExecutionDetail.tsx Просмотреть файл

@@ -551,6 +551,7 @@ function saveIssuePickedMap(doPickOrderId: number, map: Record<number, number>)
} }
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 29 | v1.0.1 | 2026-07-22 */
const WorkbenchGoodPickExecutionDetail: React.FC<Props> = ({ const WorkbenchGoodPickExecutionDetail: React.FC<Props> = ({
filterArgs, filterArgs,
onSwitchToRecordTab, onSwitchToRecordTab,
@@ -1076,6 +1077,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
itemId: line.item.id, itemId: line.item.id,
itemCode: line.item.code, itemCode: line.item.code,
itemName: line.item.name, itemName: line.item.name,
uomId: line.item.uomId ?? null,
uomDesc: line.item.uomDesc, uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc, uomShortDesc: line.item.uomShortDesc,
@@ -1147,6 +1149,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
itemId: line.item.id, itemId: line.item.id,
itemCode: line.item.code, itemCode: line.item.code,
itemName: line.item.name, itemName: line.item.name,
uomId: line.item.uomId ?? null,
uomDesc: line.item.uomDesc, uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc, uomShortDesc: line.item.uomShortDesc,
@@ -1688,14 +1691,91 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
}); });
return; return;
} }
// ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed) // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
const lookupStartTime = performance.now(); const lookupStartTime = performance.now();
const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
let activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
// ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
let allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
const lookupTime = performance.now() - lookupStartTime; const lookupTime = performance.now() - lookupStartTime;
console.log(` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`); console.log(` [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`);

// Case A: reject only if scanned lot UOM matches none of this item's POL UOMs
// (same item may have multiple lines with different UOMs on one pick order)
let scannedUomId = 0;
const allowedUomIds = new Set(
allLotsForItem
.map((l: any) => Number(l?.uomId))
.filter((id: number) => Number.isFinite(id) && id > 0),
);
const hasLineUoms = allowedUomIds.size > 0;
try {
const lotDetail = await fetchLotDetail(scannedStockInLineId);
scannedUomId = Number(lotDetail?.uomId) || 0;
} catch (e) {
if (hasLineUoms) {
console.warn(
"[QR PROCESS] lot-detail UOM check failed; rejecting scan",
e,
);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
t(
"This lot UOM does not match the pick line. Please scan another lot.",
),
);
});
lastProcessedQrRef.current = "";
processedQrCodesRef.current.delete(latestQr);
return;
}
}
if (
hasLineUoms &&
(!(scannedUomId > 0) || !allowedUomIds.has(scannedUomId))
) {
console.warn(
` [QR PROCESS] UOM mismatch: allowed=[${Array.from(allowedUomIds).join(",")}], scannedLot=${scannedUomId}, stockInLineId=${scannedStockInLineId}`,
);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
t(
"This lot UOM does not match the pick line. Please scan another lot.",
),
);
});
lastProcessedQrRef.current = "";
processedQrCodesRef.current.delete(latestQr);
return;
}

// Strict UOM filter — never fall back to other UOM lines
if (scannedUomId > 0 && hasLineUoms) {
allLotsForItem = allLotsForItem.filter(
(l: any) => Number(l?.uomId) === scannedUomId,
);
activeSuggestedLots = activeSuggestedLots.filter(
(l: any) => Number(l?.uomId) === scannedUomId,
);
if (allLotsForItem.length === 0) {
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(
t(
"This lot UOM does not match the pick line. Please scan another lot.",
),
);
});
lastProcessedQrRef.current = "";
processedQrCodesRef.current.delete(latestQr);
return;
}
}
// ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
// This allows users to scan other lots even when all suggested lots are rejected // This allows users to scan other lots even when all suggested lots are rejected
@@ -2646,7 +2726,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
const qrDetectionStartTime = performance.now(); const qrDetectionStartTime = performance.now();
console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`); console.log(` [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`);
console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`);
//console.log(` [QR DETECTION] Detection time: ${new Date().toISOString()}`);
console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`); console.log(` [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`);
const qrPayload = parseWorkbenchScanQrPayload(latestQr); const qrPayload = parseWorkbenchScanQrPayload(latestQr);
@@ -3457,7 +3537,7 @@ const handleStartScan = useCallback(() => {
resetScan(); resetScan();
lastConsumedQrValuesLengthRef.current = 0; lastConsumedQrValuesLengthRef.current = 0;
}, [stopScan, resetScan]); }, [stopScan, resetScan]);
// ... existing code around line 1469 ...
const handlelotnull = useCallback(async (lot: any) => { const handlelotnull = useCallback(async (lot: any) => {
// 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
const stockOutLineId = lot.stockOutLineId; const stockOutLineId = lot.stockOutLineId;
@@ -4106,7 +4186,7 @@ paginatedData.map((row, index) => {
? Number(fromPickRow) ? Number(fromPickRow)
: lockedSubmitQtyDisplay; : lockedSubmitQtyDisplay;


// 检查是否是 issue lot
const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo; const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
const rejectDisplay = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t); const rejectDisplay = buildLotRejectDisplayMessage(lot, scanRejectMessageBySolId, t);
const solSt = String(lot.stockOutLineStatus || "").toLowerCase(); const solSt = String(lot.stockOutLineStatus || "").toLowerCase();
@@ -4131,14 +4211,20 @@ paginatedData.map((row, index) => {
}} }}
> >
<TableCell> <TableCell>
<Typography variant="body2" fontWeight="bold">
<Typography variant="body1" fontWeight="bold">
{row.isGroupFirst ? row.groupDisplayIndex : ""} {row.isGroupFirst ? row.groupDisplayIndex : ""}
</Typography> </Typography>
</TableCell> </TableCell>


<TableCell>{row.isGroupFirst ? lot.itemCode : ""}</TableCell>
<TableCell> <TableCell>
<Typography variant="body1">
{row.isGroupFirst ? lot.itemCode : ""}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body1">
{row.isGroupFirst ? lot.itemName + '(' + lot.stockUnit + ')' : ""} {row.isGroupFirst ? lot.itemName + '(' + lot.stockUnit + ')' : ""}
</Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography variant="body2"> <Typography variant="body2">
@@ -4146,7 +4232,7 @@ paginatedData.map((row, index) => {
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Stack direction="row" spacing={1} alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
{(() => { {(() => {
const hasLotNo = Boolean(lot.lotNo); const hasLotNo = Boolean(lot.lotNo);
@@ -4165,7 +4251,7 @@ paginatedData.map((row, index) => {
: 'inherit'; : 'inherit';
return ( return (
<Typography <Typography
variant={isIssueText ? "body2" : "body1"}
variant={isIssueText ? "body2" : "body2"}
sx={{ sx={{
color: textColor, color: textColor,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
@@ -4213,7 +4299,7 @@ paginatedData.map((row, index) => {
sx={{ sx={{
flexShrink: 0, flexShrink: 0,
fontSize: "0.75rem", fontSize: "0.75rem",
py: 0.25,
//py: 0.25,
minWidth: "auto", minWidth: "auto",
px: 1, px: 1,
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -4556,6 +4642,12 @@ paginatedData.map((row, index) => {
).trim() || null ).trim() || null
: null : null
} }
expectedUomId={
workbenchLotLabelContextLot != null &&
Number(workbenchLotLabelContextLot.uomId) > 0
? Number(workbenchLotLabelContextLot.uomId)
: null
}
disableScanPick={workbenchLotLabelScanPickDisabled} disableScanPick={workbenchLotLabelScanPickDisabled}
onWorkbenchScanPick={handleWorkbenchLotLabelScanPick} onWorkbenchScanPick={handleWorkbenchLotLabelScanPick}
submitQty={workbenchLotLabelSubmitQty} submitQty={workbenchLotLabelSubmitQty}


+ 60
- 19
src/components/DoWorkbench/WorkbenchLotLabelPrintModal.tsx Просмотреть файл

@@ -63,6 +63,8 @@ type QrCodeAnalysisResponse = {
inventoryLotLineId: number; inventoryLotLineId: number;
warehouseCode?: string | null; warehouseCode?: string | null;
warehouseName?: string | null; warehouseName?: string | null;
uom?: string | null;
uomId?: number | null;
} | null; } | null;
sameItemLots: Array<{ sameItemLots: Array<{
lotNo: string; lotNo: string;
@@ -70,6 +72,7 @@ type QrCodeAnalysisResponse = {
stockInLineId?: number | null; stockInLineId?: number | null;
availableQty: number; availableQty: number;
uom: string; uom: string;
uomId?: number | null;
warehouseCode?: string | null; warehouseCode?: string | null;
warehouseName?: string | null; warehouseName?: string | null;
}>; }>;
@@ -95,6 +98,8 @@ export interface WorkbenchLotLabelPrintModalProps {
/** 提貨台表格列上的可用量/單位(API 的 sameItemLots 不含掃描行,需補上才能顯示「目前這筆」) */ /** 提貨台表格列上的可用量/單位(API 的 sameItemLots 不含掃描行,需補上才能顯示「目前這筆」) */
triggerLotAvailableQty?: number | null; triggerLotAvailableQty?: number | null;
triggerLotUom?: string | null; triggerLotUom?: string | null;
/** POL UomConversion id — workbench lot list only returns matching UOM */
expectedUomId?: number | null;
/** 此出庫行已掃碼/已完成時為 true,停用所有「掃碼提貨」(仍可列印標籤) */ /** 此出庫行已掃碼/已完成時為 true,停用所有「掃碼提貨」(仍可列印標籤) */
disableScanPick?: boolean; disableScanPick?: boolean;
/** /**
@@ -141,6 +146,7 @@ function isLabelPrinter(p: Printer): boolean {
return s.includes("label") && !s.includes("a4"); return s.includes("label") && !s.includes("a4");
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 30 | v1.0.1 | 2026-07-22 */
const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> = ({
open, open,
onClose, onClose,
@@ -155,6 +161,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
hideTriggeredLot = false, hideTriggeredLot = false,
triggerLotAvailableQty = null, triggerLotAvailableQty = null,
triggerLotUom = null, triggerLotUom = null,
expectedUomId = null,
disableScanPick = false, disableScanPick = false,
onWorkbenchScanPick, onWorkbenchScanPick,
submitQty = null, submitQty = null,
@@ -257,13 +264,22 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
if (id != null) setSelectedPrinterId(id); if (id != null) setSelectedPrinterId(id);
}, [open, printers, selectedPrinterId, pickDefaultPrinterId]); }, [open, printers, selectedPrinterId, pickDefaultPrinterId]);


const resolveExpectedUomId = useCallback((): number | null => {
const n = Number(expectedUomId);
return Number.isFinite(n) && n > 0 ? n : null;
}, [expectedUomId]);

const analyzePayload = useCallback( const analyzePayload = useCallback(
async (payload: ScanPayload) => { async (payload: ScanPayload) => {
setLastPayload(payload); setLastPayload(payload);
setScanError(null); setScanError(null);
setAnalysisLoading(true); setAnalysisLoading(true);
try { try {
const data = (await analyzeWorkbenchQrCode(payload)) as QrCodeAnalysisResponse;
const uomId = resolveExpectedUomId();
const data = (await analyzeWorkbenchQrCode({
...payload,
...(uomId != null ? { uomId } : {}),
})) as QrCodeAnalysisResponse;
setAnalysis(data); setAnalysis(data);
setSnackbar({ setSnackbar({
open: true, open: true,
@@ -277,7 +293,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
setAnalysisLoading(false); setAnalysisLoading(false);
} }
}, },
[],
[resolveExpectedUomId],
); );


const analyzeByItem = useCallback( const analyzeByItem = useCallback(
@@ -290,7 +306,11 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
setScanError(null); setScanError(null);
setAnalysisLoading(true); setAnalysisLoading(true);
try { try {
const data = (await fetchWorkbenchAvailableLotsByItem(itemId)) as {
const uomId = resolveExpectedUomId();
const data = (await fetchWorkbenchAvailableLotsByItem(
itemId,
uomId,
)) as {
itemId: number; itemId: number;
itemCode: string; itemCode: string;
itemName: string; itemName: string;
@@ -315,7 +335,7 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
setAnalysisLoading(false); setAnalysisLoading(false);
} }
}, },
[],
[resolveExpectedUomId],
); );


const handleAnalyze = useCallback(async () => { const handleAnalyze = useCallback(async () => {
@@ -381,30 +401,51 @@ const WorkbenchLotLabelPrintModal: React.FC<WorkbenchLotLabelPrintModalProps> =
Number.isFinite(tableQty) && tableQty >= 0 ? tableQty : 0; Number.isFinite(tableQty) && tableQty >= 0 ? tableQty : 0;
const fromApi = Number(scannedRow?.availableQty ?? 0); const fromApi = Number(scannedRow?.availableQty ?? 0);
const scanned = analysis.scanned; const scanned = analysis.scanned;
const scannedLot = scannedLotLineId
? {
lotNo: scanned?.lotNo ?? "",
inventoryLotLineId: scannedLotLineId,
stockInLineId: Number(scanned?.stockInLineId ?? 0) || null,
availableQty: Math.max(fromApi, fromTable) as number,
uom: (scannedRow?.uom ?? triggerLotUom ?? "") as string,
warehouseCode:
scanned?.warehouseCode ?? scannedRow?.warehouseCode,
warehouseName:
scanned?.warehouseName ?? scannedRow?.warehouseName,
_scanned: true as const,
}
: null;
const expectUom = Number(expectedUomId);
const hasExpectUom = Number.isFinite(expectUom) && expectUom > 0;
const scannedUomId = Number(scanned?.uomId ?? scannedRow?.uomId ?? 0);
const scannedUomOk =
!hasExpectUom ||
!Number.isFinite(scannedUomId) ||
scannedUomId <= 0 ||
scannedUomId === expectUom;
const scannedLot =
scannedLotLineId && scannedUomOk
? {
lotNo: scanned?.lotNo ?? "",
inventoryLotLineId: scannedLotLineId,
stockInLineId: Number(scanned?.stockInLineId ?? 0) || null,
availableQty: Math.max(fromApi, fromTable) as number,
uom: (scanned?.uom ?? scannedRow?.uom ?? triggerLotUom ?? "") as string,
uomId: scannedUomId > 0 ? scannedUomId : null,
warehouseCode:
scanned?.warehouseCode ?? scannedRow?.warehouseCode,
warehouseName:
scanned?.warehouseName ?? scannedRow?.warehouseName,
_scanned: true as const,
}
: null;


const merged = [ const merged = [
...(!hideTriggeredLot && scannedLot ? [scannedLot] : []), ...(!hideTriggeredLot && scannedLot ? [scannedLot] : []),
...list ...list
.filter((x) => x.inventoryLotLineId !== scannedLotLineId) .filter((x) => x.inventoryLotLineId !== scannedLotLineId)
.filter((x) => {
if (!hasExpectUom) return true;
const id = Number(x.uomId);
return !Number.isFinite(id) || id <= 0 || id === expectUom;
})
.map((x) => ({ ...x, _scanned: false as const })), .map((x) => ({ ...x, _scanned: false as const })),
]; ];


return merged; return merged;
}, [analysis, hideTriggeredLot, triggerLotAvailableQty, triggerLotUom]);
}, [
analysis,
hideTriggeredLot,
triggerLotAvailableQty,
triggerLotUom,
expectedUomId,
]);


const filteredLots = useMemo(() => { const filteredLots = useMemo(() => {
const prefix = String(warehouseCodePrefixFilter ?? "").trim(); const prefix = String(warehouseCodePrefixFilter ?? "").trim();


+ 1
- 0
src/components/InventorySearch/InventorySearch.tsx Просмотреть файл

@@ -56,6 +56,7 @@ type SearchQuery = Partial<
>; >;
type SearchParamNames = keyof SearchQuery; type SearchParamNames = keyof SearchQuery;


/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.0 | 2026-07-17 */
const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
const { t } = useTranslation(['inventory', 'common', 'item']); const { t } = useTranslation(['inventory', 'common', 'item']);




+ 8
- 3
src/components/PickOrderSearch/AssignAndRelease.tsx Просмотреть файл

@@ -50,7 +50,9 @@ interface ItemRow {
id: string; id: string;
pickOrderId: number; pickOrderId: number;
pickOrderCode: string; pickOrderCode: string;
pickOrderLineId?: number;
itemId: number; itemId: number;
uomId?: number;
itemCode: string; itemCode: string;
itemName: string; itemName: string;
requiredQty: number; requiredQty: number;
@@ -85,6 +87,7 @@ const style = {
width: { xs: "100%", sm: "100%", md: "100%" }, width: { xs: "100%", sm: "100%", md: "100%" },
}; };


/** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */
const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");
const { setIsUploading } = useUploadContext(); const { setIsUploading } = useUploadContext();
@@ -167,11 +170,13 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
// const filteredRecords = res.records.filter( // const filteredRecords = res.records.filter(
// (item: any) => (item.status || "").toLowerCase() === "pending" // (item: any) => (item.status || "").toLowerCase() === "pending"
//); //);
const itemRows: ItemRow[] = res.records.map((item: any) => ({
id: item.id,
const itemRows: ItemRow[] = res.records.map((item: any, index: number) => ({
id: item.id ?? `${item.pickOrderId}-${item.pickOrderLineId ?? item.itemId}-${index}`,
pickOrderId: item.pickOrderId, pickOrderId: item.pickOrderId,
pickOrderCode: item.pickOrderCode, pickOrderCode: item.pickOrderCode,
pickOrderLineId: item.pickOrderLineId,
itemId: item.itemId, itemId: item.itemId,
uomId: item.uomId,
itemCode: item.itemCode, itemCode: item.itemCode,
itemName: item.itemName, itemName: item.itemName,
requiredQty: item.requiredQty, requiredQty: item.requiredQty,
@@ -493,7 +498,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => {
) : ( ) : (
groupedItems.map((group) => ( groupedItems.map((group) => (
group.items.map((item, index) => ( group.items.map((item, index) => (
<TableRow key={item.id}>
<TableRow key={item.id || `${group.pickOrderId}-${item.pickOrderLineId ?? item.itemId}-${index}`}>
{/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */}
<TableCell> <TableCell>
{index === 0 ? ( {index === 0 ? (


+ 12
- 13
src/components/PickOrderSearch/CreatedItemsTable.tsx Просмотреть файл

@@ -1,6 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { import {
Box,
Typography, Typography,
Table, Table,
TableBody, TableBody,
@@ -43,15 +42,16 @@ interface Group {
interface CreatedItemsTableProps { interface CreatedItemsTableProps {
items: CreatedItem[]; items: CreatedItem[];
groups: Group[]; groups: Group[];
onItemSelect: (itemId: number, checked: boolean) => void;
onQtyChange: (itemId: number, qty: number) => void;
onGroupChange: (itemId: number, groupId: string) => void;
onItemSelect: (itemId: number, checked: boolean, uomId?: number) => void;
onQtyChange: (itemId: number, qty: number, uomId?: number) => void;
onGroupChange: (itemId: number, groupId: string, uomId?: number) => void;
pageNum: number; pageNum: number;
pageSize: number; pageSize: number;
onPageChange: (event: unknown, newPage: number) => void; onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */
const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
items, items,
groups, groups,
@@ -65,15 +65,14 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
}) => { }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");


// Calculate pagination
const startIndex = (pageNum - 1) * pageSize; const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize; const endIndex = startIndex + pageSize;
const paginatedItems = items.slice(startIndex, endIndex); const paginatedItems = items.slice(startIndex, endIndex);


const handleQtyChange = useCallback((itemId: number, value: string) => {
const handleQtyChange = useCallback((itemId: number, uomId: number, value: string) => {
const numValue = Number(value); const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1) { if (!isNaN(numValue) && numValue >= 1) {
onQtyChange(itemId, numValue);
onQtyChange(itemId, numValue, uomId);
} }
}, [onQtyChange]); }, [onQtyChange]);


@@ -117,11 +116,11 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
</TableRow> </TableRow>
) : ( ) : (
paginatedItems.map((item) => ( paginatedItems.map((item) => (
<TableRow key={item.itemId}>
<TableRow key={`${item.itemId}_${item.uomId}`}>
<TableCell padding="checkbox"> <TableCell padding="checkbox">
<Checkbox <Checkbox
checked={item.isSelected} checked={item.isSelected}
onChange={(e) => onItemSelect(item.itemId, e.target.checked)}
onChange={(e) => onItemSelect(item.itemId, e.target.checked, item.uomId)}
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -134,7 +133,7 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
<FormControl size="small" sx={{ minWidth: 120 }}> <FormControl size="small" sx={{ minWidth: 120 }}>
<Select <Select
value={item.groupId?.toString() || ""} value={item.groupId?.toString() || ""}
onChange={(e) => onGroupChange(item.itemId, e.target.value)}
onChange={(e) => onGroupChange(item.itemId, e.target.value, item.uomId)}
displayEmpty displayEmpty
> >
<MenuItem value=""> <MenuItem value="">
@@ -157,14 +156,14 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography variant="body2">{item.uomDesc}</Typography>
<Typography variant="body2">{item.uomDesc || item.uom || "-"}</Typography>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
<TextField <TextField
type="number" type="number"
size="small" size="small"
value={item.qty || ""} value={item.qty || ""}
onChange={(e) => handleQtyChange(item.itemId, e.target.value)}
onChange={(e) => handleQtyChange(item.itemId, item.uomId, e.target.value)}
inputProps={{ inputProps={{
min: 1, min: 1,
step: 1, step: 1,
@@ -208,4 +207,4 @@ const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({
); );
}; };


export default CreatedItemsTable;
export default CreatedItemsTable;

+ 55
- 70
src/components/PickOrderSearch/SearchResultsTable.tsx Просмотреть файл

@@ -1,6 +1,5 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { import {
Box,
Typography, Typography,
Table, Table,
TableBody, TableBody,
@@ -22,8 +21,12 @@ import dayjs from 'dayjs';


interface SearchItemWithQty { interface SearchItemWithQty {
id: number; id: number;
/** Unique search-row key: itemId + uomId */
rowKey: string;
label: string; label: string;
qty: number | null; qty: number | null;
uomId?: number;
uom?: string;
currentStockBalance?: number; currentStockBalance?: number;
uomDesc?: string; uomDesc?: string;
targetDate?: string | null; targetDate?: string | null;
@@ -40,17 +43,18 @@ interface SearchResultsTableProps {
items: SearchItemWithQty[]; items: SearchItemWithQty[];
selectedItemIds: (string | number)[]; selectedItemIds: (string | number)[];
groups: Group[]; groups: Group[];
onItemSelect: (itemId: number, checked: boolean) => void;
onQtyChange: (itemId: number, qty: number | null) => void;
onQtyBlur: (itemId: number) => void;
onGroupChange: (itemId: number, groupId: string) => void;
isItemInCreated: (itemId: number) => boolean;
onItemSelect: (rowKey: string, checked: boolean) => void;
onQtyChange: (rowKey: string, qty: number | null) => void;
onQtyBlur: (rowKey: string) => void;
onGroupChange: (rowKey: string, groupId: string) => void;
isItemInCreated: (itemId: number, uomId?: number) => boolean;
pageNum: number; pageNum: number;
pageSize: number; pageSize: number;
onPageChange: (event: unknown, newPage: number) => void; onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */
const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
items, items,
selectedItemIds, selectedItemIds,
@@ -67,16 +71,14 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
}) => { }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");


// Calculate pagination
const startIndex = (pageNum - 1) * pageSize; const startIndex = (pageNum - 1) * pageSize;
const endIndex = startIndex + pageSize; const endIndex = startIndex + pageSize;
const paginatedResults = items.slice(startIndex, endIndex); const paginatedResults = items.slice(startIndex, endIndex);


const handleQtyChange = useCallback((itemId: number, value: string) => {
// Only allow numbers
const handleQtyChange = useCallback((rowKey: string, value: string) => {
if (value === "" || /^\d*\.?\d+$/.test(value)) { if (value === "" || /^\d*\.?\d+$/.test(value)) {
const numValue = value === "" ? null : Number(value); const numValue = value === "" ? null : Number(value);
onQtyChange(itemId, numValue);
onQtyChange(rowKey, numValue);
} }
}, [onQtyChange]); }, [onQtyChange]);


@@ -119,50 +121,45 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
paginatedResults.map((item) => (
<TableRow key={item.id}>
paginatedResults.map((item) => {
const rowKey = item.rowKey || `${item.id}_${item.uomId || 0}`;
const inCreated = isItemInCreated(item.id, item.uomId);
return (
<TableRow key={rowKey}>
<TableCell padding="checkbox"> <TableCell padding="checkbox">
<Checkbox <Checkbox
checked={selectedItemIds.includes(item.id)}
onChange={(e) => onItemSelect(item.id, e.target.checked)}
disabled={isItemInCreated(item.id)}
checked={selectedItemIds.includes(rowKey) || inCreated}
onChange={(e) => onItemSelect(rowKey, e.target.checked)}
disabled={inCreated}
/> />
</TableCell> </TableCell>
{/* Item */}
<TableCell> <TableCell>
<Box>
<Typography variant="body2">
{item.label.split(' - ')[1] || item.label}
</Typography>
<Typography variant="caption" color="textSecondary">
{item.label.split(' - ')[0] || ''}
</Typography>
</Box>
<Typography variant="body2">
{item.label.split(' - ')[1] || item.label}
</Typography>
<Typography variant="caption" color="textSecondary">
{item.label.split(' - ')[0] || ''}
</Typography>
</TableCell> </TableCell>
{/* Group */}
<TableCell> <TableCell>
<FormControl size="small" sx={{ minWidth: 120 }}> <FormControl size="small" sx={{ minWidth: 120 }}>
<Select
<Select
value={item.groupId?.toString() || ""} value={item.groupId?.toString() || ""}
onChange={(e) => onGroupChange(item.id, e.target.value)}
onChange={(e) => onGroupChange(rowKey, e.target.value)}
displayEmpty displayEmpty
disabled={isItemInCreated(item.id)}
>
disabled={inCreated}
>
<MenuItem value=""> <MenuItem value="">
<em>{t("No Group")}</em>
<em>{t("No Group")}</em>
</MenuItem> </MenuItem>
{groups.map((group) => ( {groups.map((group) => (
<MenuItem key={group.id} value={group.id.toString()}>
<MenuItem key={group.id} value={group.id.toString()}>
{group.name} {group.name}
</MenuItem>
</MenuItem>
))} ))}
</Select>
</Select>
</FormControl> </FormControl>
</TableCell>
{/* Current Stock */}
</TableCell>
<TableCell align="right"> <TableCell align="right">
<Typography <Typography
variant="body2" variant="body2"
@@ -172,54 +169,42 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
{item.currentStockBalance?.toLocaleString()||0} {item.currentStockBalance?.toLocaleString()||0}
</Typography> </Typography>
</TableCell> </TableCell>
{/* Stock Unit */}
<TableCell align="right"> <TableCell align="right">
<Typography variant="body2"> <Typography variant="body2">
{item.uomDesc || "-"}
{item.uomDesc || item.uom || "-"}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
{/* Order Quantity */}

<TextField
type="number"
size="small"
value={item.qty || ""}
onChange={(e) => {
const value = e.target.value;
// Only allow numbers
if (value === "" || /^\d+$/.test(value)) {
const numValue = value === "" ? null : Number(value);
onQtyChange(item.id, numValue);
}
}}
onBlur={() => {
// Trigger auto-add check when user finishes input (clicks elsewhere)
onQtyBlur(item.id); // ← Change this to call onQtyBlur instead!
}}
inputProps={{
<TextField
type="number"
size="small"
value={item.qty || ""}
onChange={(e) => handleQtyChange(rowKey, e.target.value)}
onBlur={() => onQtyBlur(rowKey)}
disabled={inCreated}
inputProps={{
min: 1,
max: item.currentStockBalance || 0,
step: 1,
style: { textAlign: 'center' } style: { textAlign: 'center' }
}}
sx={{
}}
sx={{
width: '80px', width: '80px',
'& .MuiInputBase-input': { '& .MuiInputBase-input': {
textAlign: 'center',
cursor: 'text'
textAlign: 'center',
cursor: 'text'
} }
}}
disabled={isItemInCreated(item.id)}
}}
/> />
</TableCell> </TableCell>
{/* Target Date */}
<TableCell align="right"> <TableCell align="right">
<Typography variant="body2"> <Typography variant="body2">
{item.targetDate ? dayjs(item.targetDate).format(OUTPUT_DATE_FORMAT) : "-"} {item.targetDate ? dayjs(item.targetDate).format(OUTPUT_DATE_FORMAT) : "-"}
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
))
);
})
)} )}
</TableBody> </TableBody>
</Table> </Table>
@@ -242,4 +227,4 @@ const SearchResultsTable: React.FC<SearchResultsTableProps> = ({
); );
}; };


export default SearchResultsTable;
export default SearchResultsTable;

+ 155
- 15
src/components/PickOrderSearch/WorkbenchPickExecution.tsx Просмотреть файл

@@ -35,6 +35,7 @@ import {
} from "@/app/api/pickOrder/actions"; } from "@/app/api/pickOrder/actions";
import { workbenchScanPick } from "@/app/api/doworkbench/actions"; import { workbenchScanPick } from "@/app/api/doworkbench/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions"; import { fetchStockInLineInfo } from "@/app/api/po/actions";
import { fetchLotDetail } from "@/app/api/inventory/actions";
import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal"; import WorkbenchLotLabelPrintModal from "@/components/DoWorkbench/WorkbenchLotLabelPrintModal";
import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider"; import TestQrCodeProvider from "../QrCodeScannerProvider/TestQrCodeProvider";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
@@ -66,6 +67,7 @@ type LineRow = {
requiredQty: number; requiredQty: number;
pickedQty: number; pickedQty: number;
stockUnit: string; stockUnit: string;
uomId?: number;
status: string; status: string;
lotsRaw: unknown[]; lotsRaw: unknown[];
}; };
@@ -89,6 +91,7 @@ type LotRow = {
itemCode: string; itemCode: string;
itemName: string; itemName: string;
uomDesc: string; uomDesc: string;
uomId?: number;
requiredQty: number; requiredQty: number;
pickOrderLineRequiredQty?: number; pickOrderLineRequiredQty?: number;
availableQty: number; availableQty: number;
@@ -273,6 +276,7 @@ function flattenLotsFromPickOrder(po: PickOrderTopRow): LotRow[] {
itemCode: line.itemCode, itemCode: line.itemCode,
itemName: line.itemName, itemName: line.itemName,
uomDesc: toStr(lot.stockUnit) || line.stockUnit, uomDesc: toStr(lot.stockUnit) || line.stockUnit,
uomId: line.uomId,
requiredQty: toNum(lot.requiredQty, line.requiredQty), requiredQty: toNum(lot.requiredQty, line.requiredQty),
pickOrderLineRequiredQty: line.requiredQty, pickOrderLineRequiredQty: line.requiredQty,
availableQty: toNum(lot.availableQty), availableQty: toNum(lot.availableQty),
@@ -333,6 +337,7 @@ function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] {
requiredQty: toNum(line?.requiredQty), requiredQty: toNum(line?.requiredQty),
pickedQty: sumLinePickedQtyFromLots(lots), pickedQty: sumLinePickedQtyFromLots(lots),
stockUnit: toStr(item?.uomDesc ?? item?.uomCode), stockUnit: toStr(item?.uomDesc ?? item?.uomCode),
uomId: toNum(item?.uomId) > 0 ? toNum(item?.uomId) : undefined,
status: toStr(line?.status), status: toStr(line?.status),
lotsRaw: lots, lotsRaw: lots,
}; };
@@ -351,6 +356,7 @@ function mapHierarchicalToPickOrders(data: unknown): PickOrderTopRow[] {
}); });
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 28 | v1.0.1 | 2026-07-22 */
const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => { const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null }; const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -770,6 +776,16 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
processedQrCodesRef.current.add(latestQr); processedQrCodesRef.current.add(latestQr);
}, []); }, []);


/** Soft-fail / switch-PO: allow the same QR to be processed again. */
const releaseProcessedQr = useCallback((qr: string) => {
const key = String(qr || "").trim();
if (!key) return;
processedQrCodesRef.current.delete(key);
if (lastProcessedQrRef.current === key) {
lastProcessedQrRef.current = "";
}
}, []);

const openUnpickableScanLotLabelModal = useCallback( const openUnpickableScanLotLabelModal = useCallback(
( (
pickRow: LotRow, pickRow: LotRow,
@@ -1177,16 +1193,102 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
return; return;
} }


const expectedPool = activeSuggestedLots.length > 0 ? activeSuggestedLots : allLotsForItem;
const expectedRow = pickExpectedRowForSubstitution(expectedPool) || allLotsForItem[0];
// Case A: reject only if scanned lot UOM matches none of this item's POL UOMs
// (same item may have multiple lines with different UOMs on one pick order)
let scannedUomId = 0;
const allowedUomIds = new Set(
allLotsForItem
.map((l) => Number(l?.uomId))
.filter((id) => Number.isFinite(id) && id > 0),
);
const hasLineUoms = allowedUomIds.size > 0;
try {
const lotDetail = await fetchLotDetail(scannedStockInLineId);
scannedUomId = Number(lotDetail?.uomId) || 0;
} catch (e) {
if (hasLineUoms) {
console.warn(
"[QR PROCESS] lot-detail UOM check failed; rejecting scan",
e,
);
const msg = t(
"This lot UOM does not match the pick line. Please scan another lot.",
);
setError(msg);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(msg);
});
releaseProcessedQr(latest);
return;
}
}
if (
hasLineUoms &&
(!(scannedUomId > 0) || !allowedUomIds.has(scannedUomId))
) {
const msg = t(
"This lot UOM does not match the pick line. Please scan another lot.",
);
setError(msg);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(msg);
});
releaseProcessedQr(latest);
return;
}

// Strict UOM filter — never fall back to other UOM lines
const filterByScannedUom = (rows: LotRow[]) => {
if (!(scannedUomId > 0) || !hasLineUoms) return rows;
return rows.filter((r) => Number(r.uomId) === scannedUomId);
};
const activeForUom = filterByScannedUom(activeSuggestedLots);
const allForUom = filterByScannedUom(allLotsForItem);
if (hasLineUoms && scannedUomId > 0 && allForUom.length === 0) {
const msg = t(
"This lot UOM does not match the pick line. Please scan another lot.",
);
setError(msg);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(msg);
});
releaseProcessedQr(latest);
return;
}
const expectedPool = activeForUom.length > 0 ? activeForUom : allForUom;
let expectedRow = pickExpectedRowForSubstitution(expectedPool) || allForUom[0];
if (!expectedRow) {
setError(t("Scanned item is not found in current line"));
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(t("Scanned item is not found in current line"));
});
releaseProcessedQr(latest);
return;
}


const scannedRows = lotRowIndexes.byStockInLineId.get(scannedStockInLineId) || []; const scannedRows = lotRowIndexes.byStockInLineId.get(scannedStockInLineId) || [];
const scannedRowInItem =
let scannedRowInItem =
scannedRows.find( scannedRows.find(
(r) => (r) =>
Number(r.itemId) === scannedItemId && Number(r.itemId) === scannedItemId &&
r.stockOutLineId > 0,
r.stockOutLineId > 0 &&
(!(scannedUomId > 0) || !hasLineUoms || Number(r.uomId) === scannedUomId),
) || ) ||
(!(scannedUomId > 0) || !hasLineUoms
? scannedRows.find(
(r) =>
Number(r.itemId) === scannedItemId &&
r.stockOutLineId > 0,
)
: undefined) ||
null; null;


if (scannedRowInItem && isRejectedStatus(scannedRowInItem.status)) { if (scannedRowInItem && isRejectedStatus(scannedRowInItem.status)) {
@@ -1231,14 +1333,46 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
return; return;
} }


if (scannedRowInItem && (isCompletedStatus(scannedRowInItem.status) || isCheckedStatus(scannedRowInItem.status))) {
setError(t("Scanned lot is already completed or checked"));
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(t("Scanned lot is already completed or checked"));
// Same lot already bound to a completed/checked SOL: only reuse for another
// pending line with the SAME uomId (never cross UOM A ↔ B).
if (
scannedRowInItem &&
(isCompletedStatus(scannedRowInItem.status) || isCheckedStatus(scannedRowInItem.status))
) {
const completedPolId = Number(scannedRowInItem.pickOrderLineId);
const completedSolId = Number(scannedRowInItem.stockOutLineId);
const pendingPoolBase = activeForUom.length > 0 ? activeForUom : allForUom;
const pendingSameUom = pendingPoolBase.filter((r) => {
if (!(r.stockOutLineId > 0)) return false;
if (isCompletedStatus(r.status) || isCheckedStatus(r.status) || isRejectedStatus(r.status)) {
return false;
}
if (Number(r.stockOutLineId) === completedSolId) return false;
if (completedPolId > 0 && Number(r.pickOrderLineId) === completedPolId) return false;
// Require explicit same UOM — do not reuse across different UOM lines
if (scannedUomId > 0 && hasLineUoms) {
return Number(r.uomId) === scannedUomId;
}
// Without line UOM metadata, do not auto-reuse (avoids A/B cross-pick)
return false;
}); });
return;
const nextExpected =
pickExpectedRowForSubstitution(pendingSameUom) || pendingSameUom[0] || null;
if (nextExpected) {
expectedRow = nextExpected;
// Force substitution path using scanned SIL, not the completed SOL row.
scannedRowInItem = null;
} else {
const msg = t("This lot has already been picked. Please scan another lot.");
setError(msg);
startTransition(() => {
setQrScanError(true);
setQrScanSuccess(false);
setQrScanErrorMsg(msg);
});
releaseProcessedQr(latest);
return;
}
} }


let scannedState: ConfirmLotState | null = null; let scannedState: ConfirmLotState | null = null;
@@ -1292,6 +1426,7 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
pickExpectedRowForSubstitution, pickExpectedRowForSubstitution,
lotRowIndexes, lotRowIndexes,
openUnpickableScanLotLabelModal, openUnpickableScanLotLabelModal,
releaseProcessedQr,
resetScan, resetScan,
submitRow, submitRow,
t, t,
@@ -1306,10 +1441,9 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [isScanning, startScan, userId]); }, [isScanning, startScan, userId]);


useEffect(() => { useEffect(() => {
if (!selectedPickOrderId) {
lastProcessedQrRef.current = "";
processedQrCodesRef.current.clear();
}
// Clear on any PO selection change (including PO1 → PO2), so the same lot QR can be scanned again.
lastProcessedQrRef.current = "";
processedQrCodesRef.current.clear();
}, [selectedPickOrderId]); }, [selectedPickOrderId]);


useEffect(() => { useEffect(() => {
@@ -1779,6 +1913,12 @@ const WorkbenchPickExecution: React.FC<Props> = ({ filterArgs }) => {
statusTitleSeverity={workbenchLotLabelStatusBanner.severity} statusTitleSeverity={workbenchLotLabelStatusBanner.severity}
triggerLotAvailableQty={workbenchLotLabelContextLot?.availableQty ?? null} triggerLotAvailableQty={workbenchLotLabelContextLot?.availableQty ?? null}
triggerLotUom={workbenchLotLabelContextLot?.uomDesc ?? null} triggerLotUom={workbenchLotLabelContextLot?.uomDesc ?? null}
expectedUomId={
workbenchLotLabelContextLot != null &&
Number(workbenchLotLabelContextLot.uomId) > 0
? Number(workbenchLotLabelContextLot.uomId)
: null
}
submitQty={ submitQty={
workbenchLotLabelContextLot?.stockOutLineId workbenchLotLabelContextLot?.stockOutLineId
? Number(resolveLockedSubmitQtyDisplay(workbenchLotLabelContextLot)) ? Number(resolveLockedSubmitQtyDisplay(workbenchLotLabelContextLot))


+ 139
- 137
src/components/PickOrderSearch/newcreatitem.tsx Просмотреть файл

@@ -70,6 +70,8 @@ interface CreatedItem {


// Add interface for search items with quantity // Add interface for search items with quantity
interface SearchItemWithQty extends ItemCombo { interface SearchItemWithQty extends ItemCombo {
/** Unique per item+uom search row */
rowKey: string;
qty: number | null; // Changed from number to number | null qty: number | null; // Changed from number to number | null
jobOrderCode?: string; jobOrderCode?: string;
jobOrderId?: number; jobOrderId?: number;
@@ -77,6 +79,10 @@ interface SearchItemWithQty extends ItemCombo {
targetDate?: string | null; // Allow null values targetDate?: string | null; // Allow null values
groupId?: number | null; // Allow null values groupId?: number | null; // Allow null values
} }

const toSearchRowKey = (itemId: number, uomId?: number | null) =>
`${itemId}_${Number(uomId) > 0 ? Number(uomId) : 0}`;

interface JobOrderDetailPickLine { interface JobOrderDetailPickLine {
id: number; id: number;
code: string; code: string;
@@ -95,8 +101,9 @@ interface Group {
} }
// Move the counter outside the component to persist across re-renders // Move the counter outside the component to persist across re-renders
let checkboxChangeCallCount = 0; let checkboxChangeCallCount = 0;
let processingItems = new Set<number>();
let processingItems = new Set<string>();


/** FP-MTMS Version Checklist | Functions Ref. No. 31 | v1.0.1 | 2026-07-22 */
const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => { const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => {
const { t } = useTranslation("pickOrder"); const { t } = useTranslation("pickOrder");
const [items, setItems] = useState<ItemCombo[]>([]); const [items, setItems] = useState<ItemCombo[]>([]);
@@ -174,6 +181,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr
uom: item.uom, uom: item.uom,
uomId: 0, uomId: 0,
uomDesc: item.uomDesc || "", // Add missing uomDesc uomDesc: item.uomDesc || "", // Add missing uomDesc
rowKey: toSearchRowKey(item.id, 0),
jobOrderCode: jobOrderDetail.code, jobOrderCode: jobOrderDetail.code,
jobOrderId: jobOrderDetail.id, jobOrderId: jobOrderDetail.id,
currentStockBalance: 0, // Add default value currentStockBalance: 0, // Add default value
@@ -263,49 +271,50 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr
); );
}, []); }, []);


// Modified handler for search item selection
// Modified handler for search item selection (legacy filteredItems path)
const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
if (isSelected) { if (isSelected) {
const item = filteredItems.find(i => i.id === itemId); const item = filteredItems.find(i => i.id === itemId);
if (!item) return; if (!item) return;
const existingItem = createdItems.find(created => created.itemId === item.id);
if (existingItem) {
const uomId = Number(item.uomId) || 0;
if (createdItems.some((c) => c.itemId === item.id && Number(c.uomId) === uomId)) {
alert(t("Item already exists in created items")); alert(t("Item already exists in created items"));
return; return;
} }
// Fix the newCreatedItem creation - add missing uomDesc
const newCreatedItem: CreatedItem = { const newCreatedItem: CreatedItem = {
itemId: item.id, itemId: item.id,
itemName: item.label, itemName: item.label,
itemCode: item.label, itemCode: item.label,
qty: item.qty || 1, qty: item.qty || 1,
uom: item.uom || "", uom: item.uom || "",
uomId: item.uomId || 0,
uomDesc: item.uomDesc || "", // Add missing uomDesc
uomId,
uomDesc: item.uomDesc || "",
isSelected: true, isSelected: true,
currentStockBalance: item.currentStockBalance, currentStockBalance: item.currentStockBalance,
targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate
groupId: item.groupId || undefined, // Handle null values
targetDate: item.targetDate || targetDate,
groupId: item.groupId || undefined,
}; };
setCreatedItems(prev => [...prev, newCreatedItem]); setCreatedItems(prev => [...prev, newCreatedItem]);
} }
}, [filteredItems, createdItems, t, targetDate]); }, [filteredItems, createdItems, t, targetDate]);


// Handler for created item selection // Handler for created item selection
const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => {
const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean, uomId?: number) => {
setCreatedItems(prev => setCreatedItems(prev =>
prev.map(item => prev.map(item =>
item.itemId === itemId ? { ...item, isSelected } : item
item.itemId === itemId &&
(uomId == null || Number(item.uomId) === Number(uomId))
? { ...item, isSelected }
: item
) )
); );
}, []); }, []);


const handleQtyChange = useCallback((itemId: number, newQty: number) => {
const handleQtyChange = useCallback((itemId: number, newQty: number, uomId?: number) => {
setCreatedItems(prev => setCreatedItems(prev =>
prev.map(item => prev.map(item =>
item.itemId === itemId
item.itemId === itemId &&
(uomId == null || Number(item.uomId) === Number(uomId))
? { ? {
...item, ...item,
qty: Math.max(1, Math.min(newQty, Math.max(0, item.currentStockBalance ?? 0))), qty: Math.max(1, Math.min(newQty, Math.max(0, item.currentStockBalance ?? 0))),
@@ -315,18 +324,23 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr
); );
}, []); }, []);


// Check if item is already in created items
const isItemInCreated = useCallback((itemId: number) => {
return createdItems.some(item => item.itemId === itemId);
// Check if item (+ optional uom) is already in created items
const isItemInCreated = useCallback((itemId: number, uomId?: number) => {
return createdItems.some(
(item) =>
item.itemId === itemId &&
(uomId == null || Number(item.uomId) === Number(uomId)),
);
}, [createdItems]); }, [createdItems]);


// 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期 // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期
const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string) => {
const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string, uomId?: number) => {
const gid = newGroupId ? Number(newGroupId) : undefined; const gid = newGroupId ? Number(newGroupId) : undefined;
const group = groups.find(g => g.id === gid); const group = groups.find(g => g.id === gid);
setCreatedItems(prev => setCreatedItems(prev =>
prev.map(it => prev.map(it =>
it.itemId === itemId
it.itemId === itemId &&
(uomId == null || Number(it.uomId) === Number(uomId))
? { ? {
...it, ...it,
groupId: gid, groupId: gid,
@@ -410,27 +424,23 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr
} }
}, [t]); }, [t]);
const checkAndAutoAddItem = useCallback((itemId: number) => {
const item = secondSearchResults.find(i => i.id === itemId);
const checkAndAutoAddItem = useCallback((rowKey: string) => {
const item = secondSearchResults.find((i) => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey);
if (!item) return; if (!item) return;


// Check if item has ALL 3 conditions:
// 1. Item is selected (checkbox checked)
const isSelected = selectedSecondSearchItemIds.includes(itemId);
// 2. Group is assigned
const key = item.rowKey || toSearchRowKey(item.id, item.uomId);
const isSelected = selectedSecondSearchItemIds.includes(key);
const hasGroup = item.groupId !== undefined && item.groupId !== null; const hasGroup = item.groupId !== undefined && item.groupId !== null;
// 3. Quantity is entered
const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0;
if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) {
// Auto-add to created items
if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id, item.uomId)) {
const newCreatedItem: CreatedItem = { const newCreatedItem: CreatedItem = {
itemId: item.id, itemId: item.id,
itemName: item.label, itemName: item.label,
itemCode: item.label, itemCode: item.label,
qty: item.qty || 1, qty: item.qty || 1,
uom: item.uom || "", uom: item.uom || "",
uomId: item.uomId || 0,
uomId: Number(item.uomId) || 0,
uomDesc: item.uomDesc || "", uomDesc: item.uomDesc || "",
isSelected: true, isSelected: true,
currentStockBalance: item.currentStockBalance, currentStockBalance: item.currentStockBalance,
@@ -438,33 +448,29 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr
groupId: item.groupId || undefined, groupId: item.groupId || undefined,
}; };
setCreatedItems(prev => [...prev, newCreatedItem]); setCreatedItems(prev => [...prev, newCreatedItem]);
// Remove from search results since it's now in created items
setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId));
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
setSecondSearchResults(prev => prev.filter((searchItem) => (searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== key));
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== key));
} }
}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
// Add this function after checkAndAutoAddItem
// Add this function after checkAndAutoAddItem
const handleQtyBlur = useCallback((itemId: number) => {
// Only auto-add if item is already selected (scenario 1: select first, then enter quantity)

const handleQtyBlur = useCallback((rowKey: string) => {
setTimeout(() => { setTimeout(() => {
const currentItem = secondSearchResults.find(i => i.id === itemId);
const currentItem = secondSearchResults.find((i) => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey);
if (!currentItem) return; if (!currentItem) return;


const isSelected = selectedSecondSearchItemIds.includes(itemId);
const key = currentItem.rowKey || toSearchRowKey(currentItem.id, currentItem.uomId);
const isSelected = selectedSecondSearchItemIds.includes(key);
const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null;
const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0;
// Only auto-add if item is already selected (scenario 1: select first, then enter quantity)
if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) {
if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id, currentItem.uomId)) {
const newCreatedItem: CreatedItem = { const newCreatedItem: CreatedItem = {
itemId: currentItem.id, itemId: currentItem.id,
itemName: currentItem.label, itemName: currentItem.label,
itemCode: currentItem.label, itemCode: currentItem.label,
qty: currentItem.qty || 1, qty: currentItem.qty || 1,
uom: currentItem.uom || "", uom: currentItem.uom || "",
uomId: currentItem.uomId || 0,
uomId: Number(currentItem.uomId) || 0,
uomDesc: currentItem.uomDesc || "", uomDesc: currentItem.uomDesc || "",
isSelected: true, isSelected: true,
currentStockBalance: currentItem.currentStockBalance, currentStockBalance: currentItem.currentStockBalance,
@@ -472,18 +478,18 @@ const handleQtyBlur = useCallback((itemId: number) => {
groupId: currentItem.groupId || undefined, groupId: currentItem.groupId || undefined,
}; };
setCreatedItems(prev => [...prev, newCreatedItem]); setCreatedItems(prev => [...prev, newCreatedItem]);
setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId));
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
setSecondSearchResults(prev => prev.filter((searchItem) => (searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== key));
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== key));
} }
}, 0); }, 0);
}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => {

const handleSearchItemGroupChange = useCallback((rowKey: string, groupId: string) => {
const gid = groupId ? Number(groupId) : undefined; const gid = groupId ? Number(groupId) : undefined;
const group = groups.find(g => g.id === gid); const group = groups.find(g => g.id === gid);
setSecondSearchResults(prev => prev.map(item => setSecondSearchResults(prev => prev.map(item =>
item.id === itemId
(item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey
? { ? {
...item, ...item,
groupId: gid, groupId: gid,
@@ -492,9 +498,8 @@ const handleQtyBlur = useCallback((itemId: number) => {
: item : item
)); ));
// Check auto-add after group assignment
setTimeout(() => { setTimeout(() => {
checkAndAutoAddItem(itemId);
checkAndAutoAddItem(rowKey);
}, 0); }, 0);
}, [groups, checkAndAutoAddItem]); }, [groups, checkAndAutoAddItem]);
// 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group)
@@ -550,6 +555,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
uomId: item.uomId, uomId: item.uomId,
uom: item.uom, uom: item.uom,
uomDesc: item.uomDesc, uomDesc: item.uomDesc,
rowKey: toSearchRowKey(item.id, item.uomId),
currentStockBalance: item.currentStockBalance, currentStockBalance: item.currentStockBalance,
qty: null, qty: null,
targetDate: targetDate, targetDate: targetDate,
@@ -1017,7 +1023,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
name: "id", name: "id",
label: "", label: "",
type: "checkbox", type: "checkbox",
disabled: (item) => isItemInCreated(item.id), // Disable if already in created items
disabled: (item) => isItemInCreated(item.id, item.uomId), // Disable if already in created items
}, },


{ {
@@ -1141,7 +1147,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
}, [formProps]); }, [formProps]);


// 添加数量变更处理函数 // 添加数量变更处理函数
const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
const handleSecondSearchQtyChange = useCallback((rowKey: string, newQty: number | null) => {
const getClampedQty = (qty: number | null, stock?: number) => { const getClampedQty = (qty: number | null, stock?: number) => {
if (qty === null) return null; if (qty === null) return null;
const maxQty = Math.max(0, stock ?? 0); const maxQty = Math.max(0, stock ?? 0);
@@ -1150,7 +1156,9 @@ const handleQtyBlur = useCallback((itemId: number) => {


setSecondSearchResults(prev => setSecondSearchResults(prev =>
prev.map(item => prev.map(item =>
item.id === itemId ? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) } : item
(item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey
? { ...item, qty: getClampedQty(newQty, item.currentStockBalance) }
: item
) )
); );
@@ -1163,24 +1171,24 @@ const handleQtyBlur = useCallback((itemId: number) => {
const newIds = ids(selectedSecondSearchItemIds); const newIds = ids(selectedSecondSearchItemIds);
setSelectedSecondSearchItemIds(newIds); setSelectedSecondSearchItemIds(newIds);
// 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面
if (newIds.length === secondSearchResults.length) { if (newIds.length === secondSearchResults.length) {
// 全选:将所有搜索结果添加到创建项目
secondSearchResults.forEach(item => { secondSearchResults.forEach(item => {
if (!isItemInCreated(item.id)) {
if (!isItemInCreated(item.id, item.uomId)) {
handleSearchItemSelect(item.id, true); handleSearchItemSelect(item.id, true);
} }
}); });
} else { } else {
// 部分选择:只处理当前页面的选择
secondSearchResults.forEach(item => { secondSearchResults.forEach(item => {
const isSelected = newIds.includes(item.id);
const isCurrentlyInCreated = isItemInCreated(item.id);
const key = item.rowKey || toSearchRowKey(item.id, item.uomId);
const isSelected = newIds.includes(key);
const isCurrentlyInCreated = isItemInCreated(item.id, item.uomId);
if (isSelected && !isCurrentlyInCreated) { if (isSelected && !isCurrentlyInCreated) {
handleSearchItemSelect(item.id, true); handleSearchItemSelect(item.id, true);
} else if (!isSelected && isCurrentlyInCreated) { } else if (!isSelected && isCurrentlyInCreated) {
setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
setCreatedItems(prev => prev.filter(createdItem =>
!(createdItem.itemId === item.id && Number(createdItem.uomId) === Number(item.uomId))
));
} }
}); });
} }
@@ -1192,13 +1200,19 @@ const handleQtyBlur = useCallback((itemId: number) => {
const newlyDeselected = previousIds.filter(id => !ids.includes(id)); const newlyDeselected = previousIds.filter(id => !ids.includes(id));
newlySelected.forEach(id => { newlySelected.forEach(id => {
if (!isItemInCreated(id as number)) {
handleSearchItemSelect(id as number, true);
const row = secondSearchResults.find(item => (item.rowKey || toSearchRowKey(item.id, item.uomId)) === String(id));
if (row && !isItemInCreated(row.id, row.uomId)) {
handleSearchItemSelect(row.id, true);
} }
}); });
newlyDeselected.forEach(id => { newlyDeselected.forEach(id => {
setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
const row = secondSearchResults.find(item => (item.rowKey || toSearchRowKey(item.id, item.uomId)) === String(id));
if (row) {
setCreatedItems(prev => prev.filter(createdItem =>
!(createdItem.itemId === row.id && Number(createdItem.uomId) === Number(row.uomId))
));
}
}); });
} }
}, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]);
@@ -1209,7 +1223,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
name: "id", name: "id",
label: "", label: "",
type: "checkbox", type: "checkbox",
disabled: (item) => isItemInCreated(item.id),
disabled: (item) => isItemInCreated(item.id, item.uomId),
}, },
{ {
name: "label", name: "label",
@@ -1260,7 +1274,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
renderCell: (item) => ( renderCell: (item) => (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
<Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */} <Typography sx={{ textAlign: 'right' }}> {/* Add right alignment for the value */}
{item.uom || "-"}
{item.uomDesc || item.uom || "-"}
</Typography> </Typography>
</Box> </Box>
), ),
@@ -1280,7 +1294,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
// Only allow numbers // Only allow numbers
if (value === "" || /^\d+$/.test(value)) { if (value === "" || /^\d+$/.test(value)) {
const numValue = value === "" ? null : Number(value); const numValue = value === "" ? null : Number(value);
handleSecondSearchQtyChange(item.id, numValue);
handleSecondSearchQtyChange(item.rowKey || toSearchRowKey(item.id, item.uomId), numValue);
} }
}} }}
inputProps={{ inputProps={{
@@ -1299,7 +1313,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
const value = e.target.value; const value = e.target.value;
const numValue = value === "" ? null : Number(value); const numValue = value === "" ? null : Number(value);
if (numValue !== null && numValue < 1) { if (numValue !== null && numValue < 1) {
handleSecondSearchQtyChange(item.id, 1); // Enforce min value
handleSecondSearchQtyChange(item.rowKey || toSearchRowKey(item.id, item.uomId), 1); // Enforce min value
} }
}} }}
/> />
@@ -1368,6 +1382,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
uomId: item.uomId, uomId: item.uomId,
uom: item.uom, uom: item.uom,
uomDesc: item.uomDesc, uomDesc: item.uomDesc,
rowKey: toSearchRowKey(item.id, item.uomId),
currentStockBalance: item.currentStockBalance, currentStockBalance: item.currentStockBalance,
qty: null, qty: null,
targetDate: undefined, targetDate: undefined,
@@ -1493,7 +1508,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
}, []); }, []);
const getValidationMessage = useCallback(() => { const getValidationMessage = useCallback(() => {
const selectedItems = secondSearchResults.filter(item => const selectedItems = secondSearchResults.filter(item =>
selectedSecondSearchItemIds.includes(item.id)
selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))
); );
const itemsWithoutGroup = selectedItems.filter(item => const itemsWithoutGroup = selectedItems.filter(item =>
@@ -1517,42 +1532,42 @@ const getValidationMessage = useCallback(() => {
// Fix the handleAddSelectedToCreatedItems function to properly clear selections // Fix the handleAddSelectedToCreatedItems function to properly clear selections
const handleAddSelectedToCreatedItems = useCallback(() => { const handleAddSelectedToCreatedItems = useCallback(() => {
const selectedItems = secondSearchResults.filter(item => const selectedItems = secondSearchResults.filter(item =>
selectedSecondSearchItemIds.includes(item.id)
selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))
); );
// Add selected items to created items with their own group info
selectedItems.forEach(item => {
if (!isItemInCreated(item.id)) {
const newCreatedItem: CreatedItem = {
itemId: item.id,
itemName: item.label,
itemCode: item.label,
qty: item.qty || 1,
uom: item.uom || "",
uomId: item.uomId || 0,
uomDesc: item.uomDesc || "",
isSelected: true,
currentStockBalance: item.currentStockBalance,
targetDate: item.targetDate || targetDate,
groupId: item.groupId || undefined,
};
setCreatedItems(prev => [...prev, newCreatedItem]);
}
});
// Clear the selection

const toAdd: CreatedItem[] = [];
const addedKeys: string[] = [];
for (const item of selectedItems) {
const key = item.rowKey || toSearchRowKey(item.id, item.uomId);
if (isItemInCreated(item.id, item.uomId)) continue;
toAdd.push({
itemId: item.id,
itemName: item.label,
itemCode: item.label,
qty: item.qty || 1,
uom: item.uom || "",
uomId: Number(item.uomId) || 0,
uomDesc: item.uomDesc || "",
isSelected: true,
currentStockBalance: item.currentStockBalance,
targetDate: item.targetDate || targetDate,
groupId: item.groupId || undefined,
});
addedKeys.push(key);
}
if (toAdd.length > 0) {
setCreatedItems((prev) => [...prev, ...toAdd]);
}
setSelectedSecondSearchItemIds([]); setSelectedSecondSearchItemIds([]);
// Remove the selected/added items from search results entirely
setSecondSearchResults(prev => prev.filter(item =>
!selectedSecondSearchItemIds.includes(item.id)
));
setSecondSearchResults((prev) =>
prev.filter((item) => !addedKeys.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))),
);
}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);


// Add a validation function to check if selected items are valid // Add a validation function to check if selected items are valid
const areSelectedItemsValid = useCallback(() => { const areSelectedItemsValid = useCallback(() => {
const selectedItems = secondSearchResults.filter(item => const selectedItems = secondSearchResults.filter(item =>
selectedSecondSearchItemIds.includes(item.id)
selectedSecondSearchItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))
); );
return selectedItems.every(item => return selectedItems.every(item =>
@@ -1566,17 +1581,17 @@ const getValidationMessage = useCallback(() => {


// Move these handlers to the component level (outside of CustomSearchResultsTable) // Move these handlers to the component level (outside of CustomSearchResultsTable)
// Handle individual checkbox change - ONLY select, don't add to created items // Handle individual checkbox change - ONLY select, don't add to created items
const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => {
const handleIndividualCheckboxChange = useCallback((rowKey: string, checked: boolean) => {
checkboxChangeCallCount++; checkboxChangeCallCount++;
if (checked) { if (checked) {
// Add to selected IDs // Add to selected IDs
setSelectedSecondSearchItemIds(prev => [...prev, itemId]);
setSelectedSecondSearchItemIds(prev => [...prev, rowKey]);
// Set the item's group and targetDate to current group when selected // Set the item's group and targetDate to current group when selected
setSecondSearchResults(prev => { setSecondSearchResults(prev => {
const updatedResults = prev.map(item => const updatedResults = prev.map(item =>
item.id === itemId
(item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey
? { ? {
...item, ...item,
groupId: selectedGroup?.id || undefined, groupId: selectedGroup?.id || undefined,
@@ -1588,29 +1603,27 @@ const getValidationMessage = useCallback(() => {
// Check if should auto-add after state update // Check if should auto-add after state update
setTimeout(() => { setTimeout(() => {
// Check if we're already processing this item // Check if we're already processing this item
if (processingItems.has(itemId)) {
//alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`);
if (processingItems.has(rowKey)) {
return; return;
} }
const updatedItem = updatedResults.find(i => i.id === itemId);
const updatedItem = updatedResults.find(i => (i.rowKey || toSearchRowKey(i.id, i.uomId)) === rowKey);
if (updatedItem) { if (updatedItem) {
const isSelected = true; // We just selected it const isSelected = true; // We just selected it
const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null;
const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0;
// Only auto-add if item has quantity (scenario 2: enter quantity first, then select) // Only auto-add if item has quantity (scenario 2: enter quantity first, then select)
if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) {
// Mark this item as being processed
processingItems.add(itemId);
if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id, updatedItem.uomId)) {
processingItems.add(rowKey);

const newCreatedItem: CreatedItem = { const newCreatedItem: CreatedItem = {
itemId: updatedItem.id, itemId: updatedItem.id,
itemName: updatedItem.label, itemName: updatedItem.label,
itemCode: updatedItem.label, itemCode: updatedItem.label,
qty: updatedItem.qty || 1, qty: updatedItem.qty || 1,
uom: updatedItem.uom || "", uom: updatedItem.uom || "",
uomId: updatedItem.uomId || 0,
uomId: Number(updatedItem.uomId) || 0,
uomDesc: updatedItem.uomDesc || "", uomDesc: updatedItem.uomDesc || "",
isSelected: true, isSelected: true,
currentStockBalance: updatedItem.currentStockBalance, currentStockBalance: updatedItem.currentStockBalance,
@@ -1618,29 +1631,16 @@ const getValidationMessage = useCallback(() => {
groupId: updatedItem.groupId || undefined, groupId: updatedItem.groupId || undefined,
}; };
setCreatedItems(prev => [...prev, newCreatedItem]); setCreatedItems(prev => [...prev, newCreatedItem]);
setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId));
setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId));
// Remove from processing set after a short delay
setSecondSearchResults(current => current.filter(searchItem =>
(searchItem.rowKey || toSearchRowKey(searchItem.id, searchItem.uomId)) !== rowKey
));
setSelectedSecondSearchItemIds(current => current.filter(id => id !== rowKey));
setTimeout(() => { setTimeout(() => {
processingItems.delete(itemId);
processingItems.delete(rowKey);
}, 100); }, 100);
} }
// Show final debug info in one alert
/*
alert(`FINAL DEBUG INFO for item ${itemId}:
Function called ${checkboxChangeCallCount} times
Is Selected: ${isSelected}
Has Group: ${hasGroup}
Has Quantity: ${hasQty}
Quantity: ${updatedItem.qty}
Group ID: ${updatedItem.groupId}
Is Item In Created: ${isItemInCreated(updatedItem.id)}
Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)}
Processing items: ${Array.from(processingItems).join(', ')}`);
*/
} }
}, 0); }, 0);
@@ -1648,11 +1648,11 @@ Processing items: ${Array.from(processingItems).join(', ')}`);
}); });
} else { } else {
// Remove from selected IDs // Remove from selected IDs
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== rowKey));
// Clear the item's group and targetDate when deselected // Clear the item's group and targetDate when deselected
setSecondSearchResults(prev => prev.map(item => setSecondSearchResults(prev => prev.map(item =>
item.id === itemId
(item.rowKey || toSearchRowKey(item.id, item.uomId)) === rowKey
? { ? {
...item, ...item,
groupId: undefined, groupId: undefined,
@@ -1668,17 +1668,19 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S
if (checked) { if (checked) {
// Select all items on current page that are not already in created items // Select all items on current page that are not already in created items
const newSelectedIds = paginatedResults const newSelectedIds = paginatedResults
.filter(item => !isItemInCreated(item.id))
.map(item => item.id);
.filter(item => !isItemInCreated(item.id, item.uomId))
.map(item => item.rowKey || toSearchRowKey(item.id, item.uomId));
setSelectedSecondSearchItemIds(prev => { setSelectedSecondSearchItemIds(prev => {
const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id));
const existingIds = prev.filter(id => !paginatedResults.some(item =>
(item.rowKey || toSearchRowKey(item.id, item.uomId)) === id
));
return [...existingIds, ...newSelectedIds]; return [...existingIds, ...newSelectedIds];
}); });
// Set group and targetDate for all selected items on current page // Set group and targetDate for all selected items on current page
setSecondSearchResults(prev => prev.map(item => setSecondSearchResults(prev => prev.map(item =>
newSelectedIds.includes(item.id)
newSelectedIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))
? { ? {
...item, ...item,
groupId: selectedGroup?.id || undefined, groupId: selectedGroup?.id || undefined,
@@ -1688,12 +1690,12 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S
)); ));
} else { } else {
// Deselect all items on current page // Deselect all items on current page
const pageItemIds = paginatedResults.map(item => item.id);
setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number)));
const pageItemIds = paginatedResults.map(item => item.rowKey || toSearchRowKey(item.id, item.uomId));
setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(String(id))));
// Clear group and targetDate for all deselected items on current page // Clear group and targetDate for all deselected items on current page
setSecondSearchResults(prev => prev.map(item => setSecondSearchResults(prev => prev.map(item =>
pageItemIds.includes(item.id)
pageItemIds.includes(item.rowKey || toSearchRowKey(item.id, item.uomId))
? { ? {
...item, ...item,
groupId: undefined, groupId: undefined,


+ 4
- 0
src/i18n/en/pickOrder.json Просмотреть файл

@@ -497,6 +497,10 @@
"Tomorrow": "Tomorrow", "Tomorrow": "Tomorrow",
"packaging": "packaging", "packaging": "packaging",
"This lot is not available, please scan another lot.": "This lot is not available, please scan another lot.", "This lot is not available, please scan another lot.": "This lot is not available, please scan another lot.",
"This lot UOM does not match the pick line. Please scan another lot.": "This lot UOM does not match the pick line. Please scan another lot.",
"Scanned lot UOM does not match pick line UOM": "This lot UOM does not match the pick line. Please scan another lot.",
"This lot has already been picked. Please scan another lot.": "This lot has already been picked. Please scan another lot.",
"Scanned lot is already completed or checked": "This lot has already been picked. Please scan another lot.",
"Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.", "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.",
"Lot is expired (expiry={{expiry}})": "Lot is expired (expiry={{expiry}})", "Lot is expired (expiry={{expiry}})": "Lot is expired (expiry={{expiry}})",
"Day After Tomorrow": "Day After Tomorrow", "Day After Tomorrow": "Day After Tomorrow",


+ 4
- 0
src/i18n/zh/pickOrder.json Просмотреть файл

@@ -507,6 +507,10 @@
"packaging": "提料中", "packaging": "提料中",
"No Stock Available": "沒有庫存可用", "No Stock Available": "沒有庫存可用",
"This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。", "This lot is not available, please scan another lot.": "此批號不可用,請掃描其他批號。",
"This lot UOM does not match the pick line. Please scan another lot.": "此批號單位與提貨行不符,請掃描其他批號。",
"Scanned lot UOM does not match pick line UOM": "此批號單位與提貨行不符,請掃描其他批號。",
"This lot has already been picked. Please scan another lot.": "此批號已提貨,請掃描其他批號。",
"Scanned lot is already completed or checked": "此物料已提貨,請掃描其他批號。",
"Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用 QR 碼。", "Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.": "請檢查周圍是否有其他可用 QR 碼。",
"Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})", "Lot is expired (expiry={{expiry}})": "掃描批號已過期(到期日={{expiry}})",
"Day After Tomorrow": "後日", "Day After Tomorrow": "後日",


Загрузка…
Отмена
Сохранить