Ver a proveniência

[Inventory] Update inventory

master
cyril.tsui há 1 mês
ascendente
cometimento
3e13a386d5
7 ficheiros alterados com 429 adições e 136 eliminações
  1. +110
    -0
      src/components/InventorySearch/InventoryLotLineTable.tsx
  2. +147
    -99
      src/components/InventorySearch/InventorySearch.tsx
  3. +111
    -0
      src/components/InventorySearch/InventoryTable.tsx
  4. +1
    -1
      src/components/SearchBox/SearchBox.tsx
  5. +49
    -34
      src/components/SearchResults/SearchResults.tsx
  6. +1
    -0
      src/i18n/zh/common.json
  7. +10
    -2
      src/i18n/zh/inventory.json

+ 110
- 0
src/components/InventorySearch/InventoryLotLineTable.tsx Ver ficheiro

@@ -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;

+ 147
- 99
src/components/InventorySearch/InventorySearch.tsx Ver ficheiro

@@ -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}
/>
</>
);


+ 111
- 0
src/components/InventorySearch/InventoryTable.tsx Ver ficheiro

@@ -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;

+ 1
- 1
src/components/SearchBox/SearchBox.tsx Ver ficheiro

@@ -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>


+ 49
- 34
src/components/SearchResults/SearchResults.tsx Ver ficheiro

@@ -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>


+ 1
- 0
src/i18n/zh/common.json Ver ficheiro

@@ -35,6 +35,7 @@
"Project":"專案",
"Product":"產品",
"Material":"材料",
"mat":"原料",
"FG":"成品",
"FG & Material Demand Forecast Detail":"成品及材料需求預測詳情",
"View item In-out And inventory Ledger":"查看物料出入庫及庫存日誌",


+ 10
- 2
src/i18n/zh/inventory.json Ver ficheiro

@@ -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": "已選擇項目"
}

Carregando…
Cancelar
Guardar