|
|
@@ -1,16 +1,36 @@ |
|
|
"use client"; |
|
|
|
|
|
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; |
|
|
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
import { isEqual, orderBy, uniq, uniqBy } from "lodash"; |
|
|
|
|
|
import SearchResults, { Column } from "../SearchResults"; |
|
|
|
|
|
import { CheckCircleOutline, DoDisturb } from "@mui/icons-material"; |
|
|
|
|
|
import InventoryTable from "./InventoryTable"; |
|
|
|
|
|
import { defaultPagingController } from "../SearchResults/SearchResults"; |
|
|
|
|
|
import InventoryLotLineTable from "./InventoryLotLineTable"; |
|
|
|
|
|
import { SearchInventory, SearchInventoryLotLine, fetchInventories, fetchInventoryLotLines } from "@/app/api/inventory/actions"; |
|
|
|
|
|
import { PrinterCombo } from "@/app/api/settings/printer"; |
|
|
|
|
|
|
|
|
'use client'; |
|
|
|
|
|
import { InventoryLotLineResult, InventoryResult } from '@/app/api/inventory'; |
|
|
|
|
|
import { useTranslation } from 'react-i18next'; |
|
|
|
|
|
import SearchBox, { Criterion } from '../SearchBox'; |
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'; |
|
|
|
|
|
import { uniq, uniqBy } from 'lodash'; |
|
|
|
|
|
import InventoryTable from './InventoryTable'; |
|
|
|
|
|
import { defaultPagingController } from '../SearchResults/SearchResults'; |
|
|
|
|
|
import InventoryLotLineTable from './InventoryLotLineTable'; |
|
|
|
|
|
import { |
|
|
|
|
|
SearchInventory, |
|
|
|
|
|
SearchInventoryLotLine, |
|
|
|
|
|
fetchInventories, |
|
|
|
|
|
fetchInventoryLotLines, |
|
|
|
|
|
} from '@/app/api/inventory/actions'; |
|
|
|
|
|
import { PrinterCombo } from '@/app/api/settings/printer'; |
|
|
|
|
|
import { ItemCombo, fetchItemsWithDetails } from '@/app/api/settings/item/actions'; |
|
|
|
|
|
import { |
|
|
|
|
|
Button, |
|
|
|
|
|
Dialog, |
|
|
|
|
|
DialogActions, |
|
|
|
|
|
DialogContent, |
|
|
|
|
|
DialogTitle, |
|
|
|
|
|
TextField, |
|
|
|
|
|
Box, |
|
|
|
|
|
CircularProgress, |
|
|
|
|
|
Table, |
|
|
|
|
|
TableBody, |
|
|
|
|
|
TableCell, |
|
|
|
|
|
TableHead, |
|
|
|
|
|
TableRow, |
|
|
|
|
|
Radio, |
|
|
|
|
|
} from '@mui/material'; |
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
inventories: InventoryResult[]; |
|
|
inventories: InventoryResult[]; |
|
|
@@ -35,7 +55,7 @@ type SearchQuery = Partial< |
|
|
type SearchParamNames = keyof SearchQuery; |
|
|
type SearchParamNames = keyof SearchQuery; |
|
|
|
|
|
|
|
|
const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
const { t } = useTranslation(["inventory", "common"]); |
|
|
|
|
|
|
|
|
const { t } = useTranslation(['inventory', 'common', 'item']); |
|
|
|
|
|
|
|
|
// Inventory |
|
|
// Inventory |
|
|
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); |
|
|
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); |
|
|
@@ -48,32 +68,42 @@ 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 defaultInputs = useMemo(() => ({ |
|
|
|
|
|
itemId: "", |
|
|
|
|
|
itemCode: "", |
|
|
|
|
|
itemName: "", |
|
|
|
|
|
itemType: "", |
|
|
|
|
|
onHandQty: "", |
|
|
|
|
|
onHoldQty: "", |
|
|
|
|
|
unavailableQty: "", |
|
|
|
|
|
availableQty: "", |
|
|
|
|
|
currencyName: "", |
|
|
|
|
|
status: "", |
|
|
|
|
|
baseUom: "", |
|
|
|
|
|
uomShortDesc: "", |
|
|
|
|
|
latestMarketUnitPrice: "", |
|
|
|
|
|
latestMupUpdatedDate: "", |
|
|
|
|
|
}), []) |
|
|
|
|
|
|
|
|
// 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( |
|
|
|
|
|
() => ({ |
|
|
|
|
|
itemId: '', |
|
|
|
|
|
itemCode: '', |
|
|
|
|
|
itemName: '', |
|
|
|
|
|
itemType: '', |
|
|
|
|
|
onHandQty: '', |
|
|
|
|
|
onHoldQty: '', |
|
|
|
|
|
unavailableQty: '', |
|
|
|
|
|
availableQty: '', |
|
|
|
|
|
currencyName: '', |
|
|
|
|
|
status: '', |
|
|
|
|
|
baseUom: '', |
|
|
|
|
|
uomShortDesc: '', |
|
|
|
|
|
latestMarketUnitPrice: '', |
|
|
|
|
|
latestMupUpdatedDate: '', |
|
|
|
|
|
}), |
|
|
|
|
|
[], |
|
|
|
|
|
); |
|
|
const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs); |
|
|
const [inputs, setInputs] = useState<Record<SearchParamNames, string>>(defaultInputs); |
|
|
|
|
|
|
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( |
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( |
|
|
() => [ |
|
|
() => [ |
|
|
{ label: t("Code"), paramName: "itemCode", type: "text" }, |
|
|
|
|
|
{ label: t("Name"), paramName: "itemName", type: "text" }, |
|
|
|
|
|
|
|
|
{ label: t('Code'), paramName: 'itemCode', type: 'text' }, |
|
|
|
|
|
{ label: t('Name'), paramName: 'itemName', type: 'text' }, |
|
|
{ |
|
|
{ |
|
|
label: t("Type"), |
|
|
|
|
|
paramName: "itemType", |
|
|
|
|
|
type: "select", |
|
|
|
|
|
|
|
|
label: t('Type'), |
|
|
|
|
|
paramName: 'itemType', |
|
|
|
|
|
type: 'select', |
|
|
options: uniq(inventories.map((i) => i.itemType)), |
|
|
options: uniq(inventories.map((i) => i.itemType)), |
|
|
}, |
|
|
}, |
|
|
// { |
|
|
// { |
|
|
@@ -83,112 +113,112 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
// options: uniq(inventories.map((i) => i.status)), |
|
|
// options: uniq(inventories.map((i) => i.status)), |
|
|
// }, |
|
|
// }, |
|
|
], |
|
|
], |
|
|
[t], |
|
|
|
|
|
|
|
|
[t, inventories], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
// Inventory |
|
|
// Inventory |
|
|
const refetchInventoryData = useCallback(async ( |
|
|
|
|
|
query: Record<SearchParamNames, string>, |
|
|
|
|
|
actionType: "reset" | "search" | "paging" | "init", |
|
|
|
|
|
pagingController: typeof defaultPagingController, |
|
|
|
|
|
) => { |
|
|
|
|
|
console.log("%c Action Type 1.", "color:red", actionType) |
|
|
|
|
|
// Avoid loading data again |
|
|
|
|
|
if (actionType === "paging" && pagingController === defaultPagingController) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
console.log("%c Action Type 2.", "color:blue", actionType) |
|
|
|
|
|
|
|
|
|
|
|
const params: SearchInventory = { |
|
|
|
|
|
code: query?.itemCode ?? '', |
|
|
|
|
|
name: query?.itemName ?? '', |
|
|
|
|
|
type: query?.itemType.toLowerCase() === "all" ? '' : query?.itemType ?? '', |
|
|
|
|
|
pageNum: pagingController.pageNum - 1, |
|
|
|
|
|
pageSize: pagingController.pageSize |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const refetchInventoryData = useCallback( |
|
|
|
|
|
async ( |
|
|
|
|
|
query: Record<SearchParamNames, string>, |
|
|
|
|
|
actionType: 'reset' | 'search' | 'paging' | 'init', |
|
|
|
|
|
pagingController: typeof defaultPagingController, |
|
|
|
|
|
) => { |
|
|
|
|
|
console.log('%c Action Type 1.', 'color:red', actionType); |
|
|
|
|
|
// Avoid loading data again |
|
|
|
|
|
if (actionType === 'paging' && pagingController === defaultPagingController) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
console.log('%c Action Type 2.', 'color:blue', actionType); |
|
|
|
|
|
|
|
|
|
|
|
const params: SearchInventory = { |
|
|
|
|
|
code: query?.itemCode ?? '', |
|
|
|
|
|
name: query?.itemName ?? '', |
|
|
|
|
|
type: query?.itemType.toLowerCase() === 'all' ? '' : query?.itemType ?? '', |
|
|
|
|
|
pageNum: pagingController.pageNum - 1, |
|
|
|
|
|
pageSize: pagingController.pageSize, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const response = await fetchInventories(params) |
|
|
|
|
|
|
|
|
|
|
|
if (response) { |
|
|
|
|
|
setInventoriesTotalCount(response.total); |
|
|
|
|
|
switch (actionType) { |
|
|
|
|
|
case "init": |
|
|
|
|
|
case "reset": |
|
|
|
|
|
case "search": |
|
|
|
|
|
setFilteredInventories(() => response.records); |
|
|
|
|
|
break; |
|
|
|
|
|
case "paging": |
|
|
|
|
|
setFilteredInventories((fi) => |
|
|
|
|
|
// orderBy( |
|
|
|
|
|
uniqBy([...fi, ...response.records], "id") |
|
|
|
|
|
// , ["id"], ["desc"]) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const response = await fetchInventories(params); |
|
|
|
|
|
|
|
|
|
|
|
if (response) { |
|
|
|
|
|
setInventoriesTotalCount(response.total); |
|
|
|
|
|
switch (actionType) { |
|
|
|
|
|
case 'init': |
|
|
|
|
|
case 'reset': |
|
|
|
|
|
case 'search': |
|
|
|
|
|
setFilteredInventories(() => response.records); |
|
|
|
|
|
break; |
|
|
|
|
|
case 'paging': |
|
|
|
|
|
setFilteredInventories((fi) => |
|
|
|
|
|
uniqBy([...fi, ...response.records], 'id'), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
[], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
refetchInventoryData(defaultInputs, "init", defaultPagingController) |
|
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
refetchInventoryData(defaultInputs, 'init', defaultPagingController); |
|
|
|
|
|
}, [defaultInputs, refetchInventoryData]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
// if (!isEqual(inventoriesPagingController, defaultPagingController)) { |
|
|
// if (!isEqual(inventoriesPagingController, defaultPagingController)) { |
|
|
refetchInventoryData(inputs, "paging", inventoriesPagingController) |
|
|
|
|
|
|
|
|
refetchInventoryData(inputs, 'paging', inventoriesPagingController) |
|
|
// } |
|
|
// } |
|
|
}, [inventoriesPagingController]) |
|
|
}, [inventoriesPagingController]) |
|
|
|
|
|
|
|
|
// Inventory Lot Line |
|
|
// Inventory Lot Line |
|
|
const refetchInventoryLotLineData = useCallback(async ( |
|
|
|
|
|
itemId: number | null, |
|
|
|
|
|
actionType: "reset" | "search" | "paging", |
|
|
|
|
|
pagingController: typeof defaultPagingController, |
|
|
|
|
|
) => { |
|
|
|
|
|
if (!itemId) { |
|
|
|
|
|
setSelectedInventory(null) |
|
|
|
|
|
setInventoryLotLinesTotalCount(0); |
|
|
|
|
|
setFilteredInventoryLotLines([]) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const refetchInventoryLotLineData = useCallback( |
|
|
|
|
|
async ( |
|
|
|
|
|
itemId: number | null, |
|
|
|
|
|
actionType: 'reset' | 'search' | 'paging', |
|
|
|
|
|
pagingController: typeof defaultPagingController, |
|
|
|
|
|
) => { |
|
|
|
|
|
if (!itemId) { |
|
|
|
|
|
setSelectedInventory(null); |
|
|
|
|
|
setInventoryLotLinesTotalCount(0); |
|
|
|
|
|
setFilteredInventoryLotLines([]); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Avoid loading data again |
|
|
|
|
|
if (actionType === "paging" && pagingController === defaultPagingController) { |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Avoid loading data again |
|
|
|
|
|
if (actionType === 'paging' && pagingController === defaultPagingController) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const params: SearchInventoryLotLine = { |
|
|
|
|
|
itemId: itemId, |
|
|
|
|
|
pageNum: pagingController.pageNum - 1, |
|
|
|
|
|
pageSize: pagingController.pageSize |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const params: SearchInventoryLotLine = { |
|
|
|
|
|
itemId, |
|
|
|
|
|
pageNum: pagingController.pageNum - 1, |
|
|
|
|
|
pageSize: pagingController.pageSize, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const response = await fetchInventoryLotLines(params) |
|
|
|
|
|
if (response) { |
|
|
|
|
|
setInventoryLotLinesTotalCount(response.total); |
|
|
|
|
|
switch (actionType) { |
|
|
|
|
|
case "reset": |
|
|
|
|
|
case "search": |
|
|
|
|
|
setFilteredInventoryLotLines(() => response.records); |
|
|
|
|
|
break; |
|
|
|
|
|
case "paging": |
|
|
|
|
|
setFilteredInventoryLotLines((fi) => |
|
|
|
|
|
// orderBy( |
|
|
|
|
|
uniqBy([...fi, ...response.records], "id"), |
|
|
|
|
|
// ["id"], ["desc"]) |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const response = await fetchInventoryLotLines(params); |
|
|
|
|
|
if (response) { |
|
|
|
|
|
setInventoryLotLinesTotalCount(response.total); |
|
|
|
|
|
switch (actionType) { |
|
|
|
|
|
case 'reset': |
|
|
|
|
|
case 'search': |
|
|
|
|
|
setFilteredInventoryLotLines(() => response.records); |
|
|
|
|
|
break; |
|
|
|
|
|
case 'paging': |
|
|
|
|
|
setFilteredInventoryLotLines((fi) => uniqBy([...fi, ...response.records], 'id')); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
[], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
// if (!isEqual(inventoryLotLinesPagingController, defaultPagingController)) { |
|
|
// if (!isEqual(inventoryLotLinesPagingController, defaultPagingController)) { |
|
|
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "paging", inventoryLotLinesPagingController) |
|
|
|
|
|
|
|
|
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, 'paging', inventoryLotLinesPagingController) |
|
|
// } |
|
|
// } |
|
|
}, [inventoryLotLinesPagingController]) |
|
|
}, [inventoryLotLinesPagingController]) |
|
|
|
|
|
|
|
|
// Reset |
|
|
// Reset |
|
|
const onReset = useCallback(() => { |
|
|
const onReset = useCallback(() => { |
|
|
refetchInventoryData(defaultInputs, "reset", defaultPagingController); |
|
|
|
|
|
refetchInventoryLotLineData(null, "reset", defaultPagingController); |
|
|
|
|
|
|
|
|
refetchInventoryData(defaultInputs, 'reset', defaultPagingController); |
|
|
|
|
|
refetchInventoryLotLineData(null, 'reset', defaultPagingController); |
|
|
// setFilteredInventories(inventories); |
|
|
// setFilteredInventories(inventories); |
|
|
|
|
|
|
|
|
setInputs(() => defaultInputs) |
|
|
setInputs(() => defaultInputs) |
|
|
@@ -197,48 +227,147 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
// Click Row |
|
|
// Click Row |
|
|
const onInventoryRowClick = useCallback((item: InventoryResult) => { |
|
|
|
|
|
refetchInventoryLotLineData(item.itemId, "search", defaultPagingController) |
|
|
|
|
|
|
|
|
|
|
|
setSelectedInventory(item) |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController) |
|
|
|
|
|
}, []) |
|
|
|
|
|
|
|
|
const onInventoryRowClick = useCallback( |
|
|
|
|
|
(item: InventoryResult) => { |
|
|
|
|
|
refetchInventoryLotLineData(item.itemId, 'search', defaultPagingController); |
|
|
|
|
|
setSelectedInventory(item); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
}, |
|
|
|
|
|
[refetchInventoryLotLineData], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// On Search |
|
|
// On Search |
|
|
const onSearch = useCallback((query: Record<SearchParamNames, string>) => { |
|
|
|
|
|
refetchInventoryData(query, "search", defaultPagingController) |
|
|
|
|
|
refetchInventoryLotLineData(null, "search", defaultPagingController); |
|
|
|
|
|
|
|
|
const onSearch = useCallback( |
|
|
|
|
|
(query: Record<SearchParamNames, string>) => { |
|
|
|
|
|
refetchInventoryData(query, 'search', defaultPagingController); |
|
|
|
|
|
refetchInventoryLotLineData(null, 'search', defaultPagingController); |
|
|
|
|
|
|
|
|
setInputs(() => query) |
|
|
|
|
|
setInventoriesPagingController(() => defaultPagingController) |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController) |
|
|
|
|
|
}, [refetchInventoryData]) |
|
|
|
|
|
|
|
|
setInputs(() => query); |
|
|
|
|
|
setInventoriesPagingController(() => defaultPagingController); |
|
|
|
|
|
setInventoryLotLinesPagingController(() => defaultPagingController); |
|
|
|
|
|
}, |
|
|
|
|
|
[refetchInventoryData, refetchInventoryLotLineData], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
console.log("", "color: #666", inventoriesPagingController) |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Try to split label into code and name if possible: "CODE - Name" |
|
|
|
|
|
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 |
|
|
criteria={searchCriteria} |
|
|
criteria={searchCriteria} |
|
|
onSearch={(query) => { |
|
|
onSearch={(query) => { |
|
|
onSearch(query) |
|
|
|
|
|
// console.log(query) |
|
|
|
|
|
// console.log(inventories) |
|
|
|
|
|
// setInputs(() => query) |
|
|
|
|
|
// refetchInventoryData(query, "search", defaultPagingController) |
|
|
|
|
|
// setFilteredInventories( |
|
|
|
|
|
// inventories.filter( |
|
|
|
|
|
// (i) => |
|
|
|
|
|
// i.itemCode.toLowerCase().includes(query.itemCode.toLowerCase()) && |
|
|
|
|
|
// i.itemName.toLowerCase().includes(query.itemName.toLowerCase()) && |
|
|
|
|
|
// (query.itemType == "All" || |
|
|
|
|
|
// i.itemType.toLowerCase().includes(query.itemType.toLowerCase())) && |
|
|
|
|
|
// (query.status == "All" || |
|
|
|
|
|
// i.status.toLowerCase().includes(query.status.toLowerCase())), |
|
|
|
|
|
// ), |
|
|
|
|
|
// ); |
|
|
|
|
|
|
|
|
onSearch(query); |
|
|
}} |
|
|
}} |
|
|
onReset={onReset} |
|
|
onReset={onReset} |
|
|
|
|
|
extraActions={ |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
color="secondary" |
|
|
|
|
|
onClick={handleOpenOpeningInventoryModal} |
|
|
|
|
|
> |
|
|
|
|
|
{t('Add entry for items without inventory')} |
|
|
|
|
|
</Button> |
|
|
|
|
|
} |
|
|
/> |
|
|
/> |
|
|
<InventoryTable |
|
|
<InventoryTable |
|
|
inventories={filteredInventories} |
|
|
inventories={filteredInventories} |
|
|
@@ -255,12 +384,112 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { |
|
|
inventory={selectedInventory} |
|
|
inventory={selectedInventory} |
|
|
printerCombo={printerCombo ?? []} |
|
|
printerCombo={printerCombo ?? []} |
|
|
onStockTransferSuccess={() => |
|
|
onStockTransferSuccess={() => |
|
|
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController) |
|
|
|
|
|
|
|
|
refetchInventoryLotLineData( |
|
|
|
|
|
selectedInventory?.itemId ?? null, |
|
|
|
|
|
'search', |
|
|
|
|
|
inventoryLotLinesPagingController, |
|
|
|
|
|
) |
|
|
} |
|
|
} |
|
|
onStockAdjustmentSuccess={() => |
|
|
onStockAdjustmentSuccess={() => |
|
|
refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController) |
|
|
|
|
|
|
|
|
refetchInventoryLotLineData( |
|
|
|
|
|
selectedInventory?.itemId ?? null, |
|
|
|
|
|
'search', |
|
|
|
|
|
inventoryLotLinesPagingController, |
|
|
|
|
|
) |
|
|
} |
|
|
} |
|
|
/> |
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<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('現有庫存')}</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> |
|
|
</> |
|
|
</> |
|
|
); |
|
|
); |
|
|
}; |
|
|
}; |
|
|
|