2 Коміти

4 змінених файлів з 135 додано та 18 видалено
  1. +1
    -0
      src/app/api/inventory/actions.ts
  2. +8
    -4
      src/components/InventorySearch/InventoryLotLineTable.tsx
  3. +123
    -14
      src/components/InventorySearch/InventorySearch.tsx
  4. +3
    -0
      src/i18n/zh/inventory.json

+ 1
- 0
src/app/api/inventory/actions.ts Переглянути файл

@@ -28,6 +28,7 @@ export interface SearchInventory extends Pageable {
code: string;
name: string;
type: string;
lotNo?: string;
}

export interface InventoryResultByPage {


+ 8
- 4
src/components/InventorySearch/InventoryLotLineTable.tsx Переглянути файл

@@ -51,6 +51,7 @@ interface Props {
pagingController: typeof defaultPagingController;
totalCount: number;
inventory: InventoryResult | null;
filterLotNo?: string;
onStockTransferSuccess?: () => void | Promise<void>;
printerCombo?: PrinterCombo[];
onStockAdjustmentSuccess?: () => void | Promise<void>;
@@ -58,6 +59,7 @@ interface Props {

const InventoryLotLineTable: React.FC<Props> = ({
inventoryLotLines, pagingController, setPagingController, totalCount, inventory,
filterLotNo,
onStockTransferSuccess, printerCombo = [],
onStockAdjustmentSuccess,
}) => {
@@ -108,10 +110,12 @@ const InventoryLotLineTable: React.FC<Props> = ({
}
}, [addEntryModalOpen]);

const availableLotLines = useMemo(
() => (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available"),
[inventoryLotLines]
);
const availableLotLines = useMemo(() => {
const base = (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available");
const f = filterLotNo?.trim?.() ? filterLotNo.trim() : '';
if (!f) return base;
return base.filter((line) => line.lotNo === f);
}, [inventoryLotLines, filterLotNo]);
const originalQty = selectedLotLine?.availableQty || 0;
const remainingQty = originalQty - qtyToBeTransferred;



+ 123
- 14
src/components/InventorySearch/InventorySearch.tsx Переглянути файл

@@ -7,7 +7,9 @@ import { uniq, uniqBy } from 'lodash';
import InventoryTable from './InventoryTable';
import { defaultPagingController } from '../SearchResults/SearchResults';
import InventoryLotLineTable from './InventoryLotLineTable';
import { useQrCodeScannerContext } from '@/components/QrCodeScannerProvider/QrCodeScannerProvider';
import {
analyzeQrCode,
SearchInventory,
SearchInventoryLotLine,
fetchInventories,
@@ -68,6 +70,15 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController)
const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0)

// Scan-mode UI (hardware QR scanner via QrCodeScannerProvider)
const qrScanner = useQrCodeScannerContext();
const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle');
const [scanHoverCancel, setScanHoverCancel] = useState(false);

// Resolved lot no for filtering
const [lotNoFilter, setLotNoFilter] = useState('');
const [scannedItemId, setScannedItemId] = useState<number | null>(null);

// Opening inventory (pure opening stock for items without existing inventory)
const [openingItems, setOpeningItems] = useState<ItemCombo[]>([]);
const [openingModalOpen, setOpeningModalOpen] = useState(false);
@@ -122,6 +133,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
query: Record<SearchParamNames, string>,
actionType: 'reset' | 'search' | 'paging' | 'init',
pagingController: typeof defaultPagingController,
lotNo: string,
) => {
console.log('%c Action Type 1.', 'color:red', actionType);
// Avoid loading data again
@@ -134,6 +146,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
code: query?.itemCode ?? '',
name: query?.itemName ?? '',
type: query?.itemType.toLowerCase() === 'all' ? '' : query?.itemType ?? '',
lotNo: lotNo?.trim() ? lotNo.trim() : undefined,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
};
@@ -154,19 +167,21 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
);
}
}

return response;
},
[],
);

useEffect(() => {
refetchInventoryData(defaultInputs, 'init', defaultPagingController);
refetchInventoryData(defaultInputs, 'init', defaultPagingController, '');
}, [defaultInputs, refetchInventoryData]);

useEffect(() => {
// if (!isEqual(inventoriesPagingController, defaultPagingController)) {
refetchInventoryData(inputs, 'paging', inventoriesPagingController)
refetchInventoryData(inputs, 'paging', inventoriesPagingController, lotNoFilter)
// }
}, [inventoriesPagingController])
}, [inventoriesPagingController, inputs, lotNoFilter, refetchInventoryData])

// Inventory Lot Line
const refetchInventoryLotLineData = useCallback(
@@ -217,14 +232,20 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {

// Reset
const onReset = useCallback(() => {
refetchInventoryData(defaultInputs, 'reset', defaultPagingController);
refetchInventoryData(defaultInputs, 'reset', defaultPagingController, '');
refetchInventoryLotLineData(null, 'reset', defaultPagingController);
// setFilteredInventories(inventories);

setLotNoFilter('');
setScannedItemId(null);
setScanUiMode('idle');
setScanHoverCancel(false);
qrScanner.stopScan();
qrScanner.resetScan();
setInputs(() => defaultInputs)
setInventoriesPagingController(() => defaultPagingController)
setInventoryLotLinesPagingController(() => defaultPagingController)
}, []);
}, [defaultInputs, qrScanner, refetchInventoryData, refetchInventoryLotLineData]);

// Click Row
const onInventoryRowClick = useCallback(
@@ -239,16 +260,86 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
// On Search
const onSearch = useCallback(
(query: Record<SearchParamNames, string>) => {
refetchInventoryData(query, 'search', defaultPagingController);
setLotNoFilter('');
setScannedItemId(null);
setScanUiMode('idle');
setScanHoverCancel(false);
qrScanner.stopScan();
qrScanner.resetScan();
refetchInventoryData(query, 'search', defaultPagingController, '');
refetchInventoryLotLineData(null, 'search', defaultPagingController);

setInputs(() => query);
setInventoriesPagingController(() => defaultPagingController);
setInventoryLotLinesPagingController(() => defaultPagingController);
},
[refetchInventoryData, refetchInventoryLotLineData],
[qrScanner, refetchInventoryData, refetchInventoryLotLineData],
);

const startLotScan = useCallback(() => {
setScanHoverCancel(false);
setScanUiMode('scanning');
qrScanner.resetScan();
qrScanner.startScan();
}, [qrScanner]);

const cancelLotScan = useCallback(() => {
qrScanner.stopScan();
qrScanner.resetScan();
setScanUiMode('idle');
setScanHoverCancel(false);
}, [qrScanner]);

useEffect(() => {
if (scanUiMode !== 'scanning') return;

const itemId = qrScanner.result?.itemId;
const stockInLineId = qrScanner.result?.stockInLineId;
if (!itemId || !stockInLineId) return;

(async () => {
try {
const res = await analyzeQrCode({
itemId: Number(itemId),
stockInLineId: Number(stockInLineId),
});

const resolvedLotNo = res?.scanned?.lotNo?.trim?.() ? res.scanned.lotNo.trim() : '';
if (!resolvedLotNo) return;

setLotNoFilter(resolvedLotNo);
setScannedItemId(res?.itemId ?? Number(itemId));

const invRes = await refetchInventoryData(inputs, 'search', defaultPagingController, resolvedLotNo);
const records = invRes?.records ?? [];
const target = records.find((r) => r.itemId === (res?.itemId ?? Number(itemId))) ?? null;

if (target) {
onInventoryRowClick(target);
} else {
refetchInventoryLotLineData(null, 'search', defaultPagingController);
setSelectedInventory(null);
}

setInventoriesPagingController(() => defaultPagingController);
setInventoryLotLinesPagingController(() => defaultPagingController);
} catch (e) {
console.error('Failed to analyze QR code:', e);
} finally {
// Always go back to initial state after a scan attempt
cancelLotScan();
}
})();
}, [
cancelLotScan,
inputs,
onInventoryRowClick,
qrScanner.result,
refetchInventoryData,
refetchInventoryLotLineData,
scanUiMode,
]);

console.log('', 'color: #666', inventoriesPagingController);

const handleOpenOpeningInventoryModal = useCallback(() => {
@@ -360,13 +451,30 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
}}
onReset={onReset}
extraActions={
<Button
variant="outlined"
color="secondary"
onClick={handleOpenOpeningInventoryModal}
>
{t('Add entry for items without inventory')}
</Button>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
{scanUiMode === 'idle' ? (
<Button variant="contained" onClick={startLotScan}>
{t('Search lot by QR code')}
</Button>
) : (
<>
<Button variant="contained" disabled sx={{ bgcolor: 'grey.400', color: 'grey.800' }}>
{t('Please scan...')}
</Button>
<Button variant="contained" color="error" onClick={cancelLotScan}>
{t('Stop QR Scan')}
</Button>
</>
)}

<Button
variant="outlined"
color="secondary"
onClick={handleOpenOpeningInventoryModal}
>
{t('Add entry for items without inventory')}
</Button>
</Box>
}
/>
<InventoryTable
@@ -382,6 +490,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => {
setPagingController={setInventoryLotLinesPagingController}
totalCount={inventoryLotLinesTotalCount}
inventory={selectedInventory}
filterLotNo={lotNoFilter}
printerCombo={printerCombo ?? []}
onStockTransferSuccess={() =>
refetchInventoryLotLineData(


+ 3
- 0
src/i18n/zh/inventory.json Переглянути файл

@@ -272,6 +272,9 @@
"Add entry for items without inventory": "為無庫存貨品新增倉存",
"Enter item code or name to search": "輸入貨品編號或名稱以搜索",
"Current Stock": "現有庫存",
"Search lot by QR code": "尋找批次(掃描二維碼)",
"Please scan...": "請掃描...",
"Stop QR Scan": "停止掃碼",
"No Data": "沒有數據"

}

Завантаження…
Відмінити
Зберегти