|
|
@@ -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, fetchItemsByPage } 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[]; |
|
|
@@ -66,8 +53,6 @@ type ItemLookupRow = { |
|
|
purchaseUnit?: string; |
|
|
purchaseUnit?: string; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
type OpeningItemRow = ItemCombo & { code: string; name: string; type?: string }; |
|
|
|
|
|
|
|
|
|
|
|
const extractItemRecords = (res: unknown): ItemLookupRow[] => { |
|
|
const extractItemRecords = (res: unknown): ItemLookupRow[] => { |
|
|
if (!res) return []; |
|
|
if (!res) return []; |
|
|
if (Array.isArray(res)) return res as ItemLookupRow[]; |
|
|
if (Array.isArray(res)) return res as ItemLookupRow[]; |
|
|
@@ -77,11 +62,13 @@ const extractItemRecords = (res: unknown): ItemLookupRow[] => { |
|
|
return []; |
|
|
return []; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.1 | 2026-09-07 */ |
|
|
|
|
|
|
|
|
/** 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 { data: session } = useSession(); |
|
|
|
|
|
const abilities = session?.abilities ?? session?.user?.abilities ?? []; |
|
|
|
|
|
const canStockAdjust = hasAbility(abilities, AUTH.INVENTORY_ADJUST); |
|
|
const searchInFlightRef = useRef(false); |
|
|
const searchInFlightRef = useRef(false); |
|
|
const openingSearchInFlightRef = useRef(false); |
|
|
|
|
|
|
|
|
|
|
|
const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { |
|
|
const buildSyntheticInventory = useCallback((item: ItemLookupRow): InventoryResult => { |
|
|
const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; |
|
|
const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; |
|
|
@@ -153,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<OpeningItemRow[]>([]); |
|
|
|
|
|
const [openingModalOpen, setOpeningModalOpen] = useState(false); |
|
|
|
|
|
const [openingSelectedItem, setOpeningSelectedItem] = useState<OpeningItemRow | null>(null); |
|
|
|
|
|
const [openingLoading, setOpeningLoading] = useState(false); |
|
|
|
|
|
const [openingSearchText, setOpeningSearchText] = useState(''); |
|
|
|
|
|
|
|
|
|
|
|
const defaultInputs = useMemo( |
|
|
const defaultInputs = useMemo( |
|
|
() => ({ |
|
|
() => ({ |
|
|
itemId: '', |
|
|
itemId: '', |
|
|
@@ -354,7 +334,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
|
|
|
// No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment. |
|
|
// No inventory rows: look up item master so 0-qty rows can be selected for stock adjustment. |
|
|
if (invRes?.records?.length === 0) { |
|
|
|
|
|
|
|
|
if (canStockAdjust && invRes?.records?.length === 0) { |
|
|
try { |
|
|
try { |
|
|
const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName); |
|
|
const items = await lookupItemsByCodeOrName(query.itemCode, query.itemName); |
|
|
const typeFilter = query.itemType?.trim(); |
|
|
const typeFilter = query.itemType?.trim(); |
|
|
@@ -385,6 +365,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
refetchInventoryLotLineData, |
|
|
refetchInventoryLotLineData, |
|
|
lookupItemsByCodeOrName, |
|
|
lookupItemsByCodeOrName, |
|
|
applyItemsAsSyntheticInventories, |
|
|
applyItemsAsSyntheticInventories, |
|
|
|
|
|
canStockAdjust, |
|
|
], |
|
|
], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
@@ -430,8 +411,12 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
onInventoryRowClick(target); |
|
|
onInventoryRowClick(target); |
|
|
} else { |
|
|
} else { |
|
|
refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
const items = await lookupItemsByCodeOrName(res?.itemCode); |
|
|
|
|
|
if (!applyItemsAsSyntheticInventories(items)) { |
|
|
|
|
|
|
|
|
if (canStockAdjust) { |
|
|
|
|
|
const items = await lookupItemsByCodeOrName(res?.itemCode); |
|
|
|
|
|
if (!applyItemsAsSyntheticInventories(items)) { |
|
|
|
|
|
setSelectedInventory(null); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
setSelectedInventory(null); |
|
|
setSelectedInventory(null); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@@ -454,73 +439,10 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
refetchInventoryLotLineData, |
|
|
refetchInventoryLotLineData, |
|
|
lookupItemsByCodeOrName, |
|
|
lookupItemsByCodeOrName, |
|
|
applyItemsAsSyntheticInventories, |
|
|
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; |
|
|
|
|
|
} |
|
|
|
|
|
if (openingSearchInFlightRef.current) return; |
|
|
|
|
|
openingSearchInFlightRef.current = true; |
|
|
|
|
|
setOpeningLoading(true); |
|
|
|
|
|
try { |
|
|
|
|
|
const items = trimmed.includes(' ') |
|
|
|
|
|
? await lookupItemsByCodeOrName(undefined, trimmed) |
|
|
|
|
|
: await lookupItemsByCodeOrName(trimmed); |
|
|
|
|
|
const combos: OpeningItemRow[] = items.map((item) => { |
|
|
|
|
|
const uom = item.uomDesc || item.uom || item.purchaseUnit || ''; |
|
|
|
|
|
return { |
|
|
|
|
|
id: item.id, |
|
|
|
|
|
label: `${item.code} - ${item.name}`, |
|
|
|
|
|
uomId: 0, |
|
|
|
|
|
uom, |
|
|
|
|
|
uomDesc: uom, |
|
|
|
|
|
code: item.code, |
|
|
|
|
|
name: item.name, |
|
|
|
|
|
type: item.type, |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
setOpeningItems(combos); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error('Failed to search items for opening inventory:', e); |
|
|
|
|
|
setOpeningItems([]); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setOpeningLoading(false); |
|
|
|
|
|
openingSearchInFlightRef.current = false; |
|
|
|
|
|
} |
|
|
|
|
|
}, [openingSearchText, lookupItemsByCodeOrName]); |
|
|
|
|
|
|
|
|
|
|
|
const handleConfirmOpeningInventory = useCallback(() => { |
|
|
|
|
|
if (!openingSelectedItem) { |
|
|
|
|
|
setOpeningModalOpen(false); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
applyItemsAsSyntheticInventories([ |
|
|
|
|
|
{ |
|
|
|
|
|
id: Number(openingSelectedItem.id), |
|
|
|
|
|
code: openingSelectedItem.code, |
|
|
|
|
|
name: openingSelectedItem.name, |
|
|
|
|
|
type: openingSelectedItem.type, |
|
|
|
|
|
uom: openingSelectedItem.uom, |
|
|
|
|
|
uomDesc: openingSelectedItem.uomDesc, |
|
|
|
|
|
}, |
|
|
|
|
|
]); |
|
|
|
|
|
setOpeningModalOpen(false); |
|
|
|
|
|
}, [openingSelectedItem, applyItemsAsSyntheticInventories]); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<> |
|
|
<> |
|
|
<SearchBox |
|
|
<SearchBox |
|
|
@@ -545,14 +467,6 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
</Button> |
|
|
</Button> |
|
|
</> |
|
|
</> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
color="secondary" |
|
|
|
|
|
onClick={handleOpenOpeningInventoryModal} |
|
|
|
|
|
> |
|
|
|
|
|
{t('Add entry for items without inventory')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
</Box> |
|
|
} |
|
|
} |
|
|
/> |
|
|
/> |
|
|
@@ -604,96 +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 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>{it.code}</TableCell> |
|
|
|
|
|
<TableCell>{it.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> |
|
|
|
|
|
</> |
|
|
</> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|