|
|
@@ -2,7 +2,7 @@ |
|
|
import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory'; |
|
|
import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import SearchBox, { Criterion } from '../SearchBox'; |
|
|
import SearchBox, { Criterion } from '../SearchBox'; |
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'; |
|
|
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
|
|
import { uniq, uniqBy } from 'lodash'; |
|
|
import { uniq, uniqBy } from 'lodash'; |
|
|
import InventoryTable from './InventoryTable'; |
|
|
import InventoryTable from './InventoryTable'; |
|
|
import { defaultPagingController } from '../SearchResults/SearchResults'; |
|
|
import { defaultPagingController } from '../SearchResults/SearchResults'; |
|
|
@@ -16,23 +16,10 @@ import { |
|
|
fetchInventoryLotLines, |
|
|
fetchInventoryLotLines, |
|
|
} from '@/app/api/inventory/actions'; |
|
|
} from '@/app/api/inventory/actions'; |
|
|
import { PrinterCombo } from '@/app/api/settings/printer'; |
|
|
import { PrinterCombo } from '@/app/api/settings/printer'; |
|
|
import { ItemCombo, fetchItemsWithDetails, ItemWithDetails } from '@/app/api/settings/item/actions'; |
|
|
|
|
|
import { |
|
|
|
|
|
Button, |
|
|
|
|
|
Dialog, |
|
|
|
|
|
DialogActions, |
|
|
|
|
|
DialogContent, |
|
|
|
|
|
DialogTitle, |
|
|
|
|
|
TextField, |
|
|
|
|
|
Box, |
|
|
|
|
|
CircularProgress, |
|
|
|
|
|
Table, |
|
|
|
|
|
TableBody, |
|
|
|
|
|
TableCell, |
|
|
|
|
|
TableHead, |
|
|
|
|
|
TableRow, |
|
|
|
|
|
Radio, |
|
|
|
|
|
} from '@mui/material'; |
|
|
|
|
|
|
|
|
import { fetchItemsByPage } from '@/app/api/settings/item/actions'; |
|
|
|
|
|
import { useSession } from 'next-auth/react'; |
|
|
|
|
|
import { AUTH, hasAbility } from '@/authorities'; |
|
|
|
|
|
import { Button, Box } from '@mui/material'; |
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
inventories: InventoryResult[]; |
|
|
inventories: InventoryResult[]; |
|
|
@@ -56,42 +43,68 @@ 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 */ |
|
|
|
|
|
|
|
|
type ItemLookupRow = { |
|
|
|
|
|
id: number; |
|
|
|
|
|
code: string; |
|
|
|
|
|
name: string; |
|
|
|
|
|
type?: string; |
|
|
|
|
|
uom?: string; |
|
|
|
|
|
uomDesc?: string; |
|
|
|
|
|
purchaseUnit?: string; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const extractItemRecords = (res: unknown): ItemLookupRow[] => { |
|
|
|
|
|
if (!res) return []; |
|
|
|
|
|
if (Array.isArray(res)) return res as ItemLookupRow[]; |
|
|
|
|
|
if (typeof res === 'object' && Array.isArray((res as { records?: unknown }).records)) { |
|
|
|
|
|
return (res as { records: ItemLookupRow[] }).records; |
|
|
|
|
|
} |
|
|
|
|
|
return []; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.3 | 2026-09-07 */ |
|
|
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']); |
|
|
|
|
|
|
|
|
const buildSyntheticInventory = useCallback( |
|
|
|
|
|
(item: ItemWithDetails): InventoryResult => ({ |
|
|
|
|
|
|
|
|
const { data: session } = useSession(); |
|
|
|
|
|
const abilities = session?.abilities ?? session?.user?.abilities ?? []; |
|
|
|
|
|
const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST); |
|
|
|
|
|
const searchInFlightRef = useRef(false); |
|
|
|
|
|
|
|
|
|
|
|
const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { |
|
|
|
|
|
const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; |
|
|
|
|
|
return { |
|
|
id: 0, |
|
|
id: 0, |
|
|
itemId: item.id, |
|
|
|
|
|
|
|
|
itemId: Number(item.id), |
|
|
itemCode: item.code, |
|
|
itemCode: item.code, |
|
|
itemName: item.name, |
|
|
itemName: item.name, |
|
|
itemType: 'Material', |
|
|
|
|
|
|
|
|
itemType: item.type || 'mat', |
|
|
onHandQty: 0, |
|
|
onHandQty: 0, |
|
|
onHoldQty: 0, |
|
|
onHoldQty: 0, |
|
|
unavailableQty: 0, |
|
|
unavailableQty: 0, |
|
|
availableQty: 0, |
|
|
availableQty: 0, |
|
|
uomCode: item.uom, |
|
|
|
|
|
uomUdfudesc: item.uomDesc, |
|
|
|
|
|
uomShortDesc: item.uom, |
|
|
|
|
|
|
|
|
uomCode: item.uom || uom, |
|
|
|
|
|
uomUdfudesc: uom, |
|
|
|
|
|
uomShortDesc: item.uom || uom, |
|
|
qtyPerSmallestUnit: 1, |
|
|
qtyPerSmallestUnit: 1, |
|
|
baseUom: item.uom, |
|
|
|
|
|
|
|
|
baseUom: uom, |
|
|
price: 0, |
|
|
price: 0, |
|
|
currencyName: '', |
|
|
currencyName: '', |
|
|
status: 'active', |
|
|
status: 'active', |
|
|
latestMarketUnitPrice: undefined, |
|
|
latestMarketUnitPrice: undefined, |
|
|
latestMupUpdatedDate: undefined, |
|
|
latestMupUpdatedDate: undefined, |
|
|
}), |
|
|
|
|
|
[], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const getFirstItemRecord = useCallback((res: any): ItemWithDetails | null => { |
|
|
|
|
|
if (!res) return null; |
|
|
|
|
|
if (Array.isArray(res)) return (res[0] as ItemWithDetails) ?? null; |
|
|
|
|
|
if (Array.isArray(res?.records)) return (res.records[0] as ItemWithDetails) ?? null; |
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
|
}; |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const lookupItemsByCodeOrName = useCallback(async (code?: string, name?: string) => { |
|
|
|
|
|
const trimmedCode = code?.trim(); |
|
|
|
|
|
const trimmedName = name?.trim(); |
|
|
|
|
|
if (!trimmedCode && !trimmedName) return []; |
|
|
|
|
|
const params: Record<string, string | number> = { pageSize: 50, pageNum: 1 }; |
|
|
|
|
|
if (trimmedCode) params.code = trimmedCode; |
|
|
|
|
|
else params.name = trimmedName as string; |
|
|
|
|
|
const itemRes = await fetchItemsByPage(params); |
|
|
|
|
|
return extractItemRecords(itemRes); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
// Inventory |
|
|
// Inventory |
|
|
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); |
|
|
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); |
|
|
@@ -104,6 +117,20 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) |
|
|
const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) |
|
|
const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) |
|
|
const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) |
|
|
|
|
|
|
|
|
|
|
|
const applyItemsAsSyntheticInventories = useCallback( |
|
|
|
|
|
(items: ItemLookupRow[]) => { |
|
|
|
|
|
if (!items.length) return false; |
|
|
|
|
|
const synthetics = items.map(buildSyntheticInventory); |
|
|
|
|
|
setFilteredInventories(synthetics); |
|
|
|
|
|
setInventoriesTotalCount(synthetics.length); |
|
|
|
|
|
setSelectedInventory(synthetics[0]); |
|
|
|
|
|
setFilteredInventoryLotLines([]); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
return true; |
|
|
|
|
|
}, |
|
|
|
|
|
[buildSyntheticInventory], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) |
|
|
// Scan-mode UI (hardware QR scanner via QrCodeScannerProvider) |
|
|
const qrScanner = useQrCodeScannerContext(); |
|
|
const qrScanner = useQrCodeScannerContext(); |
|
|
const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle'); |
|
|
const [scanUiMode, setScanUiMode] = useState<'idle' | 'scanning'>('idle'); |
|
|
@@ -113,13 +140,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
const [lotNoFilter, setLotNoFilter] = useState(''); |
|
|
const [lotNoFilter, setLotNoFilter] = useState(''); |
|
|
const [scannedItemId, setScannedItemId] = useState<number | null>(null); |
|
|
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); |
|
|
|
|
|
const [openingSelectedItem, setOpeningSelectedItem] = useState<ItemCombo | null>(null); |
|
|
|
|
|
const [openingLoading, setOpeningLoading] = useState(false); |
|
|
|
|
|
const [openingSearchText, setOpeningSearchText] = useState(''); |
|
|
|
|
|
|
|
|
|
|
|
const defaultInputs = useMemo( |
|
|
const defaultInputs = useMemo( |
|
|
() => ({ |
|
|
() => ({ |
|
|
itemId: '', |
|
|
itemId: '', |
|
|
@@ -297,46 +317,55 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
// On Search |
|
|
// On Search |
|
|
const onSearch = useCallback( |
|
|
const onSearch = useCallback( |
|
|
async (query: Record<SearchParamNames, string>) => { |
|
|
async (query: Record<SearchParamNames, string>) => { |
|
|
setLotNoFilter(''); |
|
|
|
|
|
setScannedItemId(null); |
|
|
|
|
|
setScanUiMode('idle'); |
|
|
|
|
|
setScanHoverCancel(false); |
|
|
|
|
|
qrScanner.stopScan(); |
|
|
|
|
|
qrScanner.resetScan(); |
|
|
|
|
|
const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); |
|
|
|
|
|
await refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
|
|
|
|
|
|
|
|
|
setInputs(() => query); |
|
|
|
|
|
setInventoriesPagingController(() => defaultPagingController); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
|
|
|
if (searchInFlightRef.current) return; |
|
|
|
|
|
searchInFlightRef.current = true; |
|
|
|
|
|
try { |
|
|
|
|
|
setLotNoFilter(''); |
|
|
|
|
|
setScannedItemId(null); |
|
|
|
|
|
setScanUiMode('idle'); |
|
|
|
|
|
setScanHoverCancel(false); |
|
|
|
|
|
qrScanner.stopScan(); |
|
|
|
|
|
qrScanner.resetScan(); |
|
|
|
|
|
const invRes = await refetchInventoryData(query, 'search', defaultPagingController, ''); |
|
|
|
|
|
await refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
|
|
|
|
|
|
|
|
|
setInputs(() => query); |
|
|
|
|
|
setInventoriesPagingController(() => defaultPagingController); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
|
|
|
// If there are no inventory rows, render a synthetic inventory so the "Stock Adjustment" chip can be used. |
|
|
|
|
|
if (invRes?.records?.length === 0) { |
|
|
|
|
|
try { |
|
|
|
|
|
const code = query.itemCode?.trim?.(); |
|
|
|
|
|
const name = query.itemName?.trim?.(); |
|
|
|
|
|
const lookupParams = code ? { code } : name ? { name } : null; |
|
|
|
|
|
|
|
|
|
|
|
if (lookupParams) { |
|
|
|
|
|
const itemRes = await fetchItemsWithDetails(lookupParams); |
|
|
|
|
|
const firstItem = getFirstItemRecord(itemRes); |
|
|
|
|
|
if (firstItem) { |
|
|
|
|
|
setSelectedInventory(buildSyntheticInventory(firstItem)); |
|
|
|
|
|
setFilteredInventoryLotLines([]); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
|
|
|
// No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment. |
|
|
|
|
|
if (canStockAdjust && invRes?.records?.length === 0) { |
|
|
|
|
|
try { |
|
|
|
|
|
const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName); |
|
|
|
|
|
const typeFilter = query.itemType?.trim(); |
|
|
|
|
|
let filtered = |
|
|
|
|
|
typeFilter && typeFilter.toLowerCase() !== 'all' |
|
|
|
|
|
? items.filter((it) => (it.type ?? '').toLowerCase() === typeFilter.toLowerCase()) |
|
|
|
|
|
: items; |
|
|
|
|
|
const exactCode = query.itemCode?.trim().toLowerCase(); |
|
|
|
|
|
if (exactCode) { |
|
|
|
|
|
filtered = [...filtered].sort((a, b) => { |
|
|
|
|
|
const aExact = a.code?.toLowerCase() === exactCode ? 0 : 1; |
|
|
|
|
|
const bExact = b.code?.toLowerCase() === exactCode ? 0 : 1; |
|
|
|
|
|
return aExact - bExact; |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
applyItemsAsSyntheticInventories(filtered); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error('Failed to build synthetic inventory:', e); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
|
|
|
console.error('Failed to build synthetic inventory:', e); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
searchInFlightRef.current = false; |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
[ |
|
|
[ |
|
|
qrScanner, |
|
|
qrScanner, |
|
|
refetchInventoryData, |
|
|
refetchInventoryData, |
|
|
refetchInventoryLotLineData, |
|
|
refetchInventoryLotLineData, |
|
|
buildSyntheticInventory, |
|
|
|
|
|
getFirstItemRecord, |
|
|
|
|
|
|
|
|
lookupItemsByCodeOrName, |
|
|
|
|
|
applyItemsAsSyntheticInventories, |
|
|
|
|
|
canStockAdjust, |
|
|
], |
|
|
], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
@@ -382,13 +411,11 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
onInventoryRowClick(target); |
|
|
onInventoryRowClick(target); |
|
|
} else { |
|
|
} else { |
|
|
refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
// No inventory rows for this scanned item => show synthetic inventory with the existing chip workflow. |
|
|
|
|
|
const itemRes = await fetchItemsWithDetails({ code: res?.itemCode }); |
|
|
|
|
|
const firstItem = getFirstItemRecord(itemRes); |
|
|
|
|
|
if (firstItem) { |
|
|
|
|
|
setSelectedInventory(buildSyntheticInventory(firstItem)); |
|
|
|
|
|
setFilteredInventoryLotLines([]); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
|
|
|
if (canStockAdjust) { |
|
|
|
|
|
const items = await lookupItemsByCodeOrName(res?.itemCode); |
|
|
|
|
|
if (!applyItemsAsSyntheticInventories(items)) { |
|
|
|
|
|
setSelectedInventory(null); |
|
|
|
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
setSelectedInventory(null); |
|
|
setSelectedInventory(null); |
|
|
} |
|
|
} |
|
|
@@ -410,112 +437,12 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
qrScanner.result, |
|
|
qrScanner.result, |
|
|
refetchInventoryData, |
|
|
refetchInventoryData, |
|
|
refetchInventoryLotLineData, |
|
|
refetchInventoryLotLineData, |
|
|
buildSyntheticInventory, |
|
|
|
|
|
getFirstItemRecord, |
|
|
|
|
|
|
|
|
lookupItemsByCodeOrName, |
|
|
|
|
|
applyItemsAsSyntheticInventories, |
|
|
|
|
|
canStockAdjust, |
|
|
scanUiMode, |
|
|
scanUiMode, |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
//console.log('', 'color: #666', inventoriesPagingController); |
|
|
|
|
|
|
|
|
|
|
|
const handleOpenOpeningInventoryModal = useCallback(() => { |
|
|
|
|
|
setOpeningSelectedItem(null); |
|
|
|
|
|
setOpeningItems([]); |
|
|
|
|
|
setOpeningSearchText(''); |
|
|
|
|
|
setOpeningModalOpen(true); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleOpeningSearch = useCallback(async () => { |
|
|
|
|
|
const trimmed = openingSearchText.trim(); |
|
|
|
|
|
if (!trimmed) { |
|
|
|
|
|
setOpeningItems([]); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setOpeningLoading(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const searchParams: Record<string, any> = { |
|
|
|
|
|
pageSize: 50, |
|
|
|
|
|
pageNum: 1, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Heuristic: if input contains space, treat as name; otherwise treat as code. |
|
|
|
|
|
if (trimmed.includes(' ')) { |
|
|
|
|
|
searchParams.name = trimmed; |
|
|
|
|
|
} else { |
|
|
|
|
|
searchParams.code = trimmed; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const response = await fetchItemsWithDetails(searchParams); |
|
|
|
|
|
|
|
|
|
|
|
let records: any[] = []; |
|
|
|
|
|
if (response && typeof response === 'object') { |
|
|
|
|
|
const anyRes = response as any; |
|
|
|
|
|
if (Array.isArray(anyRes.records)) { |
|
|
|
|
|
records = anyRes.records; |
|
|
|
|
|
} else if (Array.isArray(anyRes)) { |
|
|
|
|
|
records = anyRes; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const combos: ItemCombo[] = records.map((item: any) => ({ |
|
|
|
|
|
id: item.id, |
|
|
|
|
|
label: `${item.code} - ${item.name}`, |
|
|
|
|
|
uomId: item.uomId, |
|
|
|
|
|
uom: item.uom, |
|
|
|
|
|
uomDesc: item.uomDesc, |
|
|
|
|
|
group: item.group, |
|
|
|
|
|
currentStockBalance: item.currentStockBalance, |
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
setOpeningItems(combos); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error('Failed to search items for opening inventory:', e); |
|
|
|
|
|
setOpeningItems([]); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setOpeningLoading(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, [openingSearchText]); |
|
|
|
|
|
|
|
|
|
|
|
const handleConfirmOpeningInventory = useCallback(() => { |
|
|
|
|
|
if (!openingSelectedItem) { |
|
|
|
|
|
setOpeningModalOpen(false); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const rawLabel = openingSelectedItem.label ?? ''; |
|
|
|
|
|
const [codePart, ...nameParts] = rawLabel.split(' - '); |
|
|
|
|
|
const itemCode = codePart?.trim() || rawLabel; |
|
|
|
|
|
const itemName = nameParts.join(' - ').trim() || itemCode; |
|
|
|
|
|
|
|
|
|
|
|
const syntheticInventory: InventoryResult = { |
|
|
|
|
|
id: 0, |
|
|
|
|
|
itemId: Number(openingSelectedItem.id), |
|
|
|
|
|
itemCode, |
|
|
|
|
|
itemName, |
|
|
|
|
|
itemType: 'Material', |
|
|
|
|
|
onHandQty: 0, |
|
|
|
|
|
onHoldQty: 0, |
|
|
|
|
|
unavailableQty: 0, |
|
|
|
|
|
availableQty: 0, |
|
|
|
|
|
uomCode: openingSelectedItem.uom, |
|
|
|
|
|
uomUdfudesc: openingSelectedItem.uomDesc, |
|
|
|
|
|
uomShortDesc: openingSelectedItem.uom, |
|
|
|
|
|
qtyPerSmallestUnit: 1, |
|
|
|
|
|
baseUom: openingSelectedItem.uom, |
|
|
|
|
|
price: 0, |
|
|
|
|
|
currencyName: '', |
|
|
|
|
|
status: 'active', |
|
|
|
|
|
latestMarketUnitPrice: undefined, |
|
|
|
|
|
latestMupUpdatedDate: undefined, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Use this synthetic inventory to drive the stock adjustment UI |
|
|
|
|
|
setSelectedInventory(syntheticInventory); |
|
|
|
|
|
setFilteredInventoryLotLines([]); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
setOpeningModalOpen(false); |
|
|
|
|
|
}, [openingSelectedItem]); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<> |
|
|
<> |
|
|
<SearchBox |
|
|
<SearchBox |
|
|
@@ -540,15 +467,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
</Button> |
|
|
</Button> |
|
|
</> |
|
|
</> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
color="secondary" |
|
|
|
|
|
onClick={handleOpenOpeningInventoryModal} |
|
|
|
|
|
sx={{ display: 'none' }} |
|
|
|
|
|
> |
|
|
|
|
|
{t('Add entry for items without inventory')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
</Box> |
|
|
} |
|
|
} |
|
|
/> |
|
|
/> |
|
|
@@ -600,98 +518,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
} |
|
|
} |
|
|
}} |
|
|
}} |
|
|
/> |
|
|
/> |
|
|
|
|
|
|
|
|
<Dialog |
|
|
|
|
|
open={openingModalOpen} |
|
|
|
|
|
onClose={() => setOpeningModalOpen(false)} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
maxWidth="md" |
|
|
|
|
|
> |
|
|
|
|
|
<DialogTitle>{t('Add entry for items without inventory')}</DialogTitle> |
|
|
|
|
|
<DialogContent sx={{ pt: 2 }}> |
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t('Item')} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
value={openingSearchText} |
|
|
|
|
|
onChange={(e) => setOpeningSearchText(e.target.value)} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (e.key === 'Enter') { |
|
|
|
|
|
e.preventDefault(); |
|
|
|
|
|
handleOpeningSearch(); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
sx={{ flex: 2 }} |
|
|
|
|
|
/> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
onClick={handleOpeningSearch} |
|
|
|
|
|
disabled={openingLoading} |
|
|
|
|
|
sx={{ flex: 1 }} |
|
|
|
|
|
> |
|
|
|
|
|
{openingLoading ? <CircularProgress size={20} /> : t('common:Search')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
|
|
|
{openingItems.length === 0 && !openingLoading ? ( |
|
|
|
|
|
<Box sx={{ py: 1, color: 'text.secondary', fontSize: 14 }}> |
|
|
|
|
|
{openingSearchText |
|
|
|
|
|
? t('No data') |
|
|
|
|
|
: t('Enter item code or name to search')} |
|
|
|
|
|
</Box> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<Table size="small"> |
|
|
|
|
|
<TableHead> |
|
|
|
|
|
<TableRow> |
|
|
|
|
|
<TableCell /> |
|
|
|
|
|
<TableCell>{t('Code')}</TableCell> |
|
|
|
|
|
<TableCell>{t('Name')}</TableCell> |
|
|
|
|
|
<TableCell>{t('UoM')}</TableCell> |
|
|
|
|
|
<TableCell align="right">{t('Current Stock')}</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
</TableHead> |
|
|
|
|
|
<TableBody> |
|
|
|
|
|
{openingItems.map((it) => { |
|
|
|
|
|
const [code, ...nameParts] = (it.label ?? '').split(' - '); |
|
|
|
|
|
const name = nameParts.join(' - '); |
|
|
|
|
|
const selected = openingSelectedItem?.id === it.id; |
|
|
|
|
|
return ( |
|
|
|
|
|
<TableRow |
|
|
|
|
|
key={it.id} |
|
|
|
|
|
hover |
|
|
|
|
|
selected={selected} |
|
|
|
|
|
onClick={() => setOpeningSelectedItem(it)} |
|
|
|
|
|
sx={{ cursor: 'pointer' }} |
|
|
|
|
|
> |
|
|
|
|
|
<TableCell padding="checkbox"> |
|
|
|
|
|
<Radio checked={selected} /> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell>{code}</TableCell> |
|
|
|
|
|
<TableCell>{name}</TableCell> |
|
|
|
|
|
<TableCell>{it.uomDesc || it.uom}</TableCell> |
|
|
|
|
|
<TableCell align="right"> |
|
|
|
|
|
{it.currentStockBalance != null ? it.currentStockBalance : '-'} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
); |
|
|
|
|
|
})} |
|
|
|
|
|
</TableBody> |
|
|
|
|
|
</Table> |
|
|
|
|
|
)} |
|
|
|
|
|
</DialogContent> |
|
|
|
|
|
<DialogActions> |
|
|
|
|
|
<Button onClick={() => setOpeningModalOpen(false)}> |
|
|
|
|
|
{t('common:Cancel')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
onClick={handleConfirmOpeningInventory} |
|
|
|
|
|
disabled={!openingSelectedItem} |
|
|
|
|
|
> |
|
|
|
|
|
{t('common:Confirm')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</DialogActions> |
|
|
|
|
|
</Dialog> |
|
|
|
|
|
</> |
|
|
</> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|