@@ -0,0 +1,110 @@ | |||
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; | |||
import { Dispatch, SetStateAction, useMemo } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Column } from "../SearchResults"; | |||
import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; | |||
import { CheckCircleOutline, DoDisturb } from "@mui/icons-material"; | |||
import { arrayToDateString } from "@/app/utils/formatUtil"; | |||
import { Typography } from "@mui/material"; | |||
import { isFinite } from "lodash"; | |||
interface Props { | |||
inventoryLotLines: InventoryLotLineResult[] | null; | |||
setPagingController: defaultSetPagingController; | |||
pagingController: typeof defaultPagingController; | |||
totalCount: number; | |||
item: InventoryResult | null; | |||
} | |||
const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, item }) => { | |||
const { t } = useTranslation(["inventory"]); | |||
const columns = useMemo<Column<InventoryLotLineResult>[]>( | |||
() => [ | |||
// { | |||
// name: "item", | |||
// label: t("Code"), | |||
// renderCell: (params) => { | |||
// return params.item.code; | |||
// }, | |||
// }, | |||
// { | |||
// name: "item", | |||
// label: t("Name"), | |||
// renderCell: (params) => { | |||
// return params.item.name; | |||
// }, | |||
// }, | |||
{ | |||
name: "lotNo", | |||
label: t("Lot No"), | |||
}, | |||
// { | |||
// name: "item", | |||
// label: t("Type"), | |||
// renderCell: (params) => { | |||
// return t(params.item.type); | |||
// }, | |||
// }, | |||
{ | |||
name: "availableQty", | |||
label: t("Available Qty"), | |||
align: "right", | |||
headerAlign: "right", | |||
type: "integer", | |||
}, | |||
{ | |||
name: "uom", | |||
label: t("Sales UoM"), | |||
align: "left", | |||
headerAlign: "left", | |||
}, | |||
{ | |||
name: "qtyPerSmallestUnit", | |||
label: t("Available Qty Per Smallest Unit"), | |||
align: "right", | |||
headerAlign: "right", | |||
type: "integer", | |||
}, | |||
{ | |||
name: "baseUom", | |||
label: t("Base UoM"), | |||
align: "left", | |||
headerAlign: "left", | |||
}, | |||
{ | |||
name: "expiryDate", | |||
label: t("Expiry Date"), | |||
renderCell: (params) => { | |||
return arrayToDateString(params.expiryDate) | |||
}, | |||
}, | |||
{ | |||
name: "status", | |||
label: t("Status"), | |||
type: "icon", | |||
icons: { | |||
available: <CheckCircleOutline fontSize="small"/>, | |||
unavailable: <DoDisturb fontSize="small"/>, | |||
}, | |||
colors: { | |||
available: "success", | |||
unavailable: "error", | |||
} | |||
}, | |||
], | |||
[t], | |||
); | |||
return <> | |||
<Typography variant="h6">{item ? `${t("Item selected")}: ${item.itemCode} | ${item.itemName} (${t(item.itemType)})` : t("No items are selected yet.")}</Typography> | |||
<SearchResults<InventoryLotLineResult> | |||
items={inventoryLotLines ?? []} | |||
columns={columns} | |||
pagingController={pagingController} | |||
setPagingController={setPagingController} | |||
totalCount={totalCount} | |||
/> | |||
</> | |||
} | |||
export default InventoryLotLineTable; |
@@ -1,11 +1,15 @@ | |||
"use client"; | |||
import { InventoryResult } from "@/app/api/inventory"; | |||
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; | |||
import { useTranslation } from "react-i18next"; | |||
import SearchBox, { Criterion } from "../SearchBox"; | |||
import { useCallback, useMemo, useState } from "react"; | |||
import { uniq } from "lodash"; | |||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { 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"; | |||
interface Props { | |||
inventories: InventoryResult[]; | |||
@@ -31,7 +35,30 @@ type SearchParamNames = keyof SearchQuery; | |||
const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||
const { t } = useTranslation(["inventory", "common"]); | |||
const [filteredInventories, setFilteredInventories] = useState(inventories); | |||
// Inventory | |||
const [filteredInventories, setFilteredInventories] = useState<InventoryResult[]>([]); | |||
const [inventoriesPagingController, setInventoriesPagingController] = useState(defaultPagingController) | |||
const [inventoriesTotalCount, setInventoriesTotalCount] = useState(0) | |||
const [item, setItem] = useState<InventoryResult | null>(null) | |||
// Inventory Lot Line | |||
const [filteredInventoryLotLines, setFilteredInventoryLotLines] = useState<InventoryLotLineResult[]>([]); | |||
const [inventoryLotLinesPagingController, setInventoryLotLinesPagingController] = useState(defaultPagingController) | |||
const [inventoryLotLinesTotalCount, setInventoryLotLinesTotalCount] = useState(0) | |||
const [inputs, setInputs] = useState<Record<SearchParamNames, string>>({ | |||
itemId: "", | |||
itemCode: "", | |||
itemName: "", | |||
itemType: "", | |||
onHandQty: "", | |||
onHoldQty: "", | |||
unavailableQty: "", | |||
availableQty: "", | |||
currencyName: "", | |||
status: "", | |||
baseUom: "", | |||
}); | |||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
() => [ | |||
@@ -43,88 +70,102 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||
type: "select", | |||
options: uniq(inventories.map((i) => i.itemType)), | |||
}, | |||
{ | |||
label: t("Status"), | |||
paramName: "status", | |||
type: "select", | |||
options: uniq(inventories.map((i) => i.status)), | |||
}, | |||
// { | |||
// label: t("Status"), | |||
// paramName: "status", | |||
// type: "select", | |||
// options: uniq(inventories.map((i) => i.status)), | |||
// }, | |||
], | |||
[t], | |||
); | |||
const refetchInventoryData = useCallback(async ( | |||
query: Record<SearchParamNames, string>, | |||
actionType: "reset" | "search" | "paging" | |||
) => { | |||
const params: SearchInventory = { | |||
code: query?.itemCode ?? '', | |||
name: query?.itemName ?? '', | |||
type: query?.itemType.toLowerCase() === "all" ? '' : query?.itemType ?? '', | |||
pageNum: inventoriesPagingController.pageNum - 1, | |||
pageSize: inventoriesPagingController.pageSize | |||
} | |||
const response = await fetchInventories(params) | |||
if (response) { | |||
console.log(response) | |||
setInventoriesTotalCount(response.total); | |||
switch (actionType) { | |||
case "reset": | |||
case "search": | |||
setFilteredInventories(() => response.records); | |||
break; | |||
case "paging": | |||
setFilteredInventories((fi) => | |||
orderBy( | |||
uniqBy([...fi, ...response.records], "id"), | |||
["id"], ["desc"] | |||
)); | |||
} | |||
} | |||
}, [inventoriesPagingController, setInventoriesPagingController]) | |||
useEffect(() => { | |||
refetchInventoryData(inputs, "paging") | |||
}, [inventoriesPagingController]) | |||
const refetchInventoryLotLineData = useCallback(async ( | |||
itemId: number | null, | |||
actionType: "reset" | "search" | "paging" | |||
) => { | |||
if (!itemId) { | |||
setItem(null) | |||
setInventoryLotLinesTotalCount(0); | |||
setFilteredInventoryLotLines([]) | |||
return | |||
} | |||
const params: SearchInventoryLotLine = { | |||
itemId: itemId, | |||
pageNum: inventoriesPagingController.pageNum - 1, | |||
pageSize: inventoriesPagingController.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"] | |||
)); | |||
} | |||
} | |||
}, [inventoriesPagingController, setInventoriesPagingController]) | |||
useEffect(() => { | |||
refetchInventoryData(inputs, "paging") | |||
}, [inventoriesPagingController]) | |||
const onReset = useCallback(() => { | |||
setFilteredInventories(inventories); | |||
}, [inventories]); | |||
refetchInventoryData(inputs, "reset"); | |||
refetchInventoryLotLineData(null, "reset"); | |||
// setFilteredInventories(inventories); | |||
}, []); | |||
const columns = useMemo<Column<InventoryResult>[]>( | |||
() => [ | |||
{ | |||
name: "itemCode", | |||
label: t("Code"), | |||
}, | |||
{ | |||
name: "itemName", | |||
label: t("Name"), | |||
}, | |||
{ | |||
name: "itemType", | |||
label: t("Type"), | |||
renderCell: (params) => { | |||
return t(params.itemType); | |||
}, | |||
}, | |||
{ | |||
name: "availableQty", | |||
label: t("Qty"), | |||
align: "right", | |||
headerAlign: "right", | |||
type: "integer", | |||
}, | |||
{ | |||
name: "uomUdfudesc", | |||
label: t("UoM"), | |||
}, | |||
// { | |||
// name: "qtyPerSmallestUnit", | |||
// label: t("Qty Per Smallest Unit"), | |||
// align: "right", | |||
// headerAlign: "right", | |||
// type: "decimal" | |||
// }, | |||
// { | |||
// name: "smallestUnit", | |||
// label: t("Smallest Unit"), | |||
// }, | |||
// { | |||
// name: "price", | |||
// label: t("Price"), | |||
// align: "right", | |||
// sx: { | |||
// alignItems: "right", | |||
// justifyContent: "end", | |||
// } | |||
// }, | |||
// { | |||
// name: "currencyName", | |||
// label: t("Currency"), | |||
// }, | |||
// { | |||
// name: "status", | |||
// label: t("Status"), | |||
// type: "icon", | |||
// icons: { | |||
// available: <CheckCircleOutline fontSize="small"/>, | |||
// unavailable: <DoDisturb fontSize="small"/>, | |||
// }, | |||
// colors: { | |||
// available: "success", | |||
// unavailable: "error", | |||
// } | |||
// }, | |||
], | |||
[t], | |||
); | |||
const onInventoryRowClick = useCallback((item: InventoryResult) => { | |||
setItem(item) | |||
refetchInventoryLotLineData(item.itemId, "search") | |||
}, []) | |||
return ( | |||
<> | |||
@@ -133,28 +174,35 @@ const InventorySearch: React.FC<Props> = ({ inventories }) => { | |||
onSearch={(query) => { | |||
// console.log(query) | |||
// console.log(inventories) | |||
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())), | |||
), | |||
); | |||
setInputs(() => query) | |||
refetchInventoryData(query, "search") | |||
// 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())), | |||
// ), | |||
// ); | |||
}} | |||
onReset={onReset} | |||
/> | |||
<SearchResults<InventoryResult> | |||
items={filteredInventories} | |||
columns={columns} | |||
pagingController={{ | |||
pageNum: 0, | |||
pageSize: 0, | |||
// totalCount: 0, | |||
}} | |||
<InventoryTable | |||
inventories={filteredInventories} | |||
pagingController={inventoriesPagingController} | |||
setPagingController={setInventoriesPagingController} | |||
totalCount={inventoriesTotalCount} | |||
onRowClick={onInventoryRowClick} | |||
/> | |||
<InventoryLotLineTable | |||
inventoryLotLines={filteredInventoryLotLines} | |||
pagingController={inventoryLotLinesPagingController} | |||
setPagingController={setInventoryLotLinesPagingController} | |||
totalCount={inventoryLotLinesTotalCount} | |||
item={item} | |||
/> | |||
</> | |||
); | |||
@@ -0,0 +1,111 @@ | |||
import { InventoryResult } from "@/app/api/inventory"; | |||
import { Dispatch, SetStateAction, useMemo } from "react"; | |||
import { useTranslation } from "react-i18next"; | |||
import { Column } from "../SearchResults"; | |||
import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; | |||
interface Props { | |||
inventories: InventoryResult[]; | |||
setPagingController: defaultSetPagingController; | |||
pagingController: typeof defaultPagingController; | |||
totalCount: number; | |||
onRowClick: (item: InventoryResult) => void; | |||
} | |||
const InventoryTable: React.FC<Props> = ({ inventories, pagingController, setPagingController, totalCount, onRowClick }) => { | |||
const { t } = useTranslation(["inventory"]); | |||
const columns = useMemo<Column<InventoryResult>[]>( | |||
() => [ | |||
{ | |||
name: "itemCode", | |||
label: t("Code"), | |||
}, | |||
{ | |||
name: "itemName", | |||
label: t("Name"), | |||
}, | |||
{ | |||
name: "itemType", | |||
label: t("Type"), | |||
renderCell: (params) => { | |||
return t(params.itemType); | |||
}, | |||
}, | |||
{ | |||
name: "availableQty", | |||
label: t("Available Qty"), | |||
align: "right", | |||
headerAlign: "right", | |||
type: "integer", | |||
}, | |||
{ | |||
name: "uomUdfudesc", | |||
label: t("Sales UoM"), | |||
align: "left", | |||
headerAlign: "left", | |||
}, | |||
{ | |||
name: "qtyPerSmallestUnit", | |||
label: t("Available Qty Per Smallest Unit"), | |||
align: "right", | |||
headerAlign: "right", | |||
type: "integer", | |||
}, | |||
{ | |||
name: "baseUom", | |||
label: t("Base UoM"), | |||
align: "left", | |||
headerAlign: "left", | |||
}, | |||
// { | |||
// name: "qtyPerSmallestUnit", | |||
// label: t("Qty Per Smallest Unit"), | |||
// align: "right", | |||
// headerAlign: "right", | |||
// type: "decimal" | |||
// }, | |||
// { | |||
// name: "smallestUnit", | |||
// label: t("Smallest Unit"), | |||
// }, | |||
// { | |||
// name: "price", | |||
// label: t("Price"), | |||
// align: "right", | |||
// sx: { | |||
// alignItems: "right", | |||
// justifyContent: "end", | |||
// } | |||
// }, | |||
// { | |||
// name: "currencyName", | |||
// label: t("Currency"), | |||
// }, | |||
// { | |||
// name: "status", | |||
// label: t("Status"), | |||
// type: "icon", | |||
// icons: { | |||
// available: <CheckCircleOutline fontSize="small"/>, | |||
// unavailable: <DoDisturb fontSize="small"/>, | |||
// }, | |||
// colors: { | |||
// available: "success", | |||
// unavailable: "error", | |||
// } | |||
// }, | |||
], | |||
[t], | |||
); | |||
return <SearchResults<InventoryResult> | |||
items={inventories} | |||
columns={columns} | |||
pagingController={pagingController} | |||
setPagingController={setPagingController} | |||
totalCount={totalCount} | |||
onRowClick={onRowClick} | |||
/> | |||
} | |||
export default InventoryTable; |
@@ -248,7 +248,7 @@ function SearchBox<T extends string>({ | |||
<MenuItem value={"All"}>{t("All")}</MenuItem> | |||
{c.options.map((option) => ( | |||
<MenuItem key={option} value={option}> | |||
{option} | |||
{t(option)} | |||
</MenuItem> | |||
))} | |||
</Select> | |||
@@ -100,6 +100,7 @@ interface Props<T extends ResultWithId> { | |||
isAutoPaging?: boolean; | |||
checkboxIds?: (string | number)[]; | |||
setCheckboxIds?: Dispatch<SetStateAction<(string | number)[]>>; | |||
onRowClick?: (item: T) => void; | |||
} | |||
function isActionColumn<T extends ResultWithId>( | |||
@@ -138,8 +139,8 @@ function convertObjectKeysToLowercase<T extends object>( | |||
): object | undefined { | |||
return obj | |||
? Object.fromEntries( | |||
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), | |||
) | |||
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]), | |||
) | |||
: undefined; | |||
} | |||
@@ -174,6 +175,14 @@ export const defaultPagingController: { pageNum: number; pageSize: number } = { | |||
pageNum: 1, | |||
pageSize: 10, | |||
}; | |||
export type defaultSetPagingController = Dispatch< | |||
SetStateAction<{ | |||
pageNum: number; | |||
pageSize: number; | |||
}> | |||
> | |||
function SearchResults<T extends ResultWithId>({ | |||
items, | |||
columns, | |||
@@ -184,6 +193,7 @@ function SearchResults<T extends ResultWithId>({ | |||
totalCount, | |||
checkboxIds = [], | |||
setCheckboxIds = undefined, | |||
onRowClick = undefined, | |||
}: Props<T>) { | |||
const [page, setPage] = React.useState(0); | |||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||
@@ -279,40 +289,25 @@ function SearchResults<T extends ResultWithId>({ | |||
<TableBody> | |||
{isAutoPaging | |||
? items | |||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
.map((item) => { | |||
return ( | |||
<TableRow | |||
hover | |||
tabIndex={-1} | |||
key={item.id} | |||
onClick={ | |||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
.map((item) => { | |||
return ( | |||
<TableRow | |||
hover | |||
tabIndex={-1} | |||
key={item.id} | |||
onClick={(event) => { | |||
setCheckboxIds | |||
? (event) => handleRowClick(event, item, columns) | |||
? handleRowClick(event, item, columns) | |||
: undefined | |||
if (onRowClick) { | |||
onRowClick(item) | |||
} | |||
} | |||
role={setCheckboxIds ? "checkbox" : undefined} | |||
> | |||
{columns.map((column, idx) => { | |||
const columnName = column.name; | |||
return ( | |||
<TabelCells | |||
key={`${columnName.toString()}-${idx}`} | |||
column={column} | |||
columnName={columnName} | |||
idx={idx} | |||
item={item} | |||
checkboxIds={checkboxIds} | |||
/> | |||
); | |||
})} | |||
</TableRow> | |||
); | |||
}) | |||
: items.map((item) => { | |||
return ( | |||
<TableRow hover tabIndex={-1} key={item.id}> | |||
} | |||
role={setCheckboxIds ? "checkbox" : undefined} | |||
> | |||
{columns.map((column, idx) => { | |||
const columnName = column.name; | |||
@@ -329,7 +324,27 @@ function SearchResults<T extends ResultWithId>({ | |||
})} | |||
</TableRow> | |||
); | |||
})} | |||
}) | |||
: items.map((item) => { | |||
return ( | |||
<TableRow hover tabIndex={-1} key={item.id}> | |||
{columns.map((column, idx) => { | |||
const columnName = column.name; | |||
return ( | |||
<TabelCells | |||
key={`${columnName.toString()}-${idx}`} | |||
column={column} | |||
columnName={columnName} | |||
idx={idx} | |||
item={item} | |||
checkboxIds={checkboxIds} | |||
/> | |||
); | |||
})} | |||
</TableRow> | |||
); | |||
})} | |||
</TableBody> | |||
</Table> | |||
</TableContainer> | |||
@@ -35,6 +35,7 @@ | |||
"Project":"專案", | |||
"Product":"產品", | |||
"Material":"材料", | |||
"mat":"原料", | |||
"FG":"成品", | |||
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情", | |||
"View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌", | |||
@@ -7,5 +7,13 @@ | |||
"Qty": "數量", | |||
"UoM": "單位", | |||
"mat": "物料", | |||
"fg": "成品" | |||
} | |||
"fg": "成品", | |||
"Available Qty": "可用數量 (銷售單位)", | |||
"Sales UoM": "銷售單位", | |||
"Available Qty Per Smallest Unit": "可用數量 (基本單位)", | |||
"Base UoM": "基本單位", | |||
"Lot No": "批號", | |||
"Expiry Date": "到期日", | |||
"No items are selected yet.": "未選擇項目", | |||
"Item selected": "已選擇項目" | |||
} |