diff --git a/src/app/(main)/inventory/page.tsx b/src/app/(main)/inventory/page.tsx
new file mode 100644
index 0000000..5016aba
--- /dev/null
+++ b/src/app/(main)/inventory/page.tsx
@@ -0,0 +1,34 @@
+import { preloadInventory } from "@/app/api/inventory";
+import InventorySearch from "@/components/InventorySearch";
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography } from "@mui/material";
+import { Metadata } from "next";
+import { Suspense } from "react";
+
+export const metadata: Metadata = {
+ title: "Inventory"
+}
+
+const Inventory: React.FC = async () => {
+ const { t } = await getServerI18n("inventory")
+
+ preloadInventory()
+
+ return <>
+
+
+ {t("Inventory")}
+
+
+ }>
+
+
+ >;
+}
+
+export default Inventory;
\ No newline at end of file
diff --git a/src/app/(main)/scheduling/detail/page.tsx b/src/app/(main)/scheduling/detail/page.tsx
index d6eced6..3116e8a 100644
--- a/src/app/(main)/scheduling/detail/page.tsx
+++ b/src/app/(main)/scheduling/detail/page.tsx
@@ -1,19 +1,16 @@
import { TypeEnum } from "@/app/utils/typeEnum";
-import ItemsSearch from "@/components/ItemsSearch";
+import DetailSchedule from "@/components/DetailSchedule";
import { getServerI18n } from "@/i18n";
-import Add from "@mui/icons-material/Add";
-import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
-import Link from "next/link";
import { Suspense } from "react";
export const metadata: Metadata = {
title: "Detail Scheduling",
};
-const detailScheduling: React.FC = async () => {
+const DetailScheduling: React.FC = async () => {
const project = TypeEnum.PRODUCT
const { t } = await getServerI18n(project);
// preloadClaims();
@@ -29,20 +26,12 @@ const detailScheduling: React.FC = async () => {
{t("Detail Scheduling")}
- {/* }
- LinkComponent={Link}
- href="product/create"
- >
- {t("Create product")}
- */}
- }>
-
+ }>
+
>
);
};
-export default detailScheduling;
+export default DetailScheduling;
diff --git a/src/app/api/inventory/actions.ts b/src/app/api/inventory/actions.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/api/inventory/index.ts b/src/app/api/inventory/index.ts
new file mode 100644
index 0000000..5425b24
--- /dev/null
+++ b/src/app/api/inventory/index.ts
@@ -0,0 +1,30 @@
+import { serverFetchJson } from "@/app/utils/fetchUtil";
+import { BASE_API_URL } from "@/config/api";
+import { cache } from "react";
+import "server-only";
+
+export interface InventoryResult {
+ id: number;
+ code: string;
+ name: string;
+ type: string;
+ qty: number;
+ uomCode: string;
+ uomUdfudesc: string;
+ germPerSmallestUnit: number;
+ qtyPerSmallestUnit: number;
+ smallestUnit: string;
+ price: number;
+ currencyName: string;
+ status: string;
+}
+
+export const preloadInventory = () => {
+ fetchInventories();
+}
+
+export const fetchInventories = cache(async() => {
+ return serverFetchJson(`${BASE_API_URL}/inventory/list`, {
+ next: { tags: ["inventories"]}
+ })
+})
\ No newline at end of file
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
index 24c3688..65f399c 100644
--- a/src/components/Breadcrumb/Breadcrumb.tsx
+++ b/src/components/Breadcrumb/Breadcrumb.tsx
@@ -18,6 +18,7 @@ const pathToLabelMap: { [path: string]: string } = {
"/scheduling/rough/edit": "FG & Material Demand Forecast Detail",
"/scheduling/detail": "Detail Scheduling",
"/scheduling/detail/edit": "FG Production Schedule",
+ "/inventory": "Inventory",
};
const Breadcrumb = () => {
diff --git a/src/components/DetailScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailScheduleDetail/ViewByFGDetails.tsx
index f706291..e40cbd2 100644
--- a/src/components/DetailScheduleDetail/ViewByFGDetails.tsx
+++ b/src/components/DetailScheduleDetail/ViewByFGDetails.tsx
@@ -101,14 +101,14 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => {
() => [
[
{
- id: 1, jobNo: "JO20250507001", priority: 85, code: "PP1193", type: "FG", name: "蔥油(1磅) ", inStockQty: 1322, productionQty: 661,
+ id: 1, jobNo: "JO20250507001", estimatedProductionTime: "1 hr", priority: 85, code: "PP1193", type: "FG", name: "蔥油(1磅) ", inStockQty: 1322, productionQty: 661,
lines: [
{ id: 1, code: "MH0040", type: "Material", name: "大豆油(1噸/桶)", inStockQty: 100, purchaseQty: 20 },
{ id: 2, code: "FA0161", type: "Material", name: "洋蔥粒", inStockQty: 80, purchaseQty: 10 }
]
},
{
- id: 2, jobNo: "JO20250507002", priority: 80, code: " PP1096", type: "FG", name: "白麵撈", inStockQty: 1040, productionQty: 520,
+ id: 2, jobNo: "JO20250507002", estimatedProductionTime: "2 hrs", priority: 80, code: " PP1096", type: "FG", name: "白麵撈", inStockQty: 1040, productionQty: 520,
lines: [
{ id: 1, code: "MH0040", type: "Material", name: "大豆油(1噸/桶)", inStockQty: 1000, purchaseQty: 190.00 },
{ id: 1, code: "MH0040", type: "Material", name: "星加坡綠富貴花牌幼白麵粉 (50磅/包)", inStockQty: 1000, purchaseQty: 250.00 },
@@ -116,7 +116,7 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => {
]
},
{
- id: 3, jobNo: "JO20250507003", priority: 35, code: "PP1080", type: "FG", name: "咖哩汁", inStockQty: 2400, productionQty: 1200.0,
+ id: 3, jobNo: "JO20250507003", estimatedProductionTime: "5 hrs : 15 mins", priority: 35, code: "PP1080", type: "FG", name: "咖哩汁", inStockQty: 2400, productionQty: 1200.0,
lines: [
{ id: 1, code: "MH0040", type: "Material", name: "大豆油(1噸/桶)", inStockQty: 0, purchaseQty: 108.88 },
{ id: 2, code: "GI3236", type: "Material", name: "清水(煮過牛腩)", inStockQty: 317.52, purchaseQty: 635.04 },
@@ -132,7 +132,7 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => {
]
},
{
- id: 4, jobNo: "JO20250507004", priority: 20, code: " PP1188", type: "FG", name: "咖喱膽", inStockQty: 1016.2, productionQty: 508.1,
+ id: 4, jobNo: "JO20250507004", estimatedProductionTime: "3 hrs", priority: 20, code: " PP1188", type: "FG", name: "咖喱膽", inStockQty: 1016.2, productionQty: 508.1,
lines: [
{ id: 1, code: "MH0040", type: "Material", name: "大豆油(1噸/桶)", inStockQty: 0, purchaseQty: 217.72 },
{ id: 2, code: "FA0161", type: "Material", name: "洋蔥粒", inStockQty: 0, purchaseQty: 18.15 },
@@ -525,6 +525,14 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => {
return row.productionQty
}
},
+ {
+ field: "estimatedProductionTime",
+ label: "Estimated Production Time",
+ type: "read-only",
+ style: {
+ textAlign: "right",
+ }
+ },
{
field: "priority",
label: "Production Priority",
diff --git a/src/components/InventorySearch/InventorySearch.tsx b/src/components/InventorySearch/InventorySearch.tsx
new file mode 100644
index 0000000..e0f3fec
--- /dev/null
+++ b/src/components/InventorySearch/InventorySearch.tsx
@@ -0,0 +1,139 @@
+"use client"
+import { 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 SearchResults, { Column } from "../SearchResults";
+import { CheckCircleOutline, DoDisturb } from "@mui/icons-material";
+
+interface Props {
+ inventories: InventoryResult[];
+}
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const InventorySearch: React.FC = ({
+ inventories,
+}) => {
+ const { t } = useTranslation("inventories");
+
+ const [filteredInventories, setFilteredInventories] = useState(inventories)
+
+ const searchCriteria: Criterion[] = useMemo(() => [
+ { label: t("Code"), paramName: "code", type: "text" },
+ { label: t("Name"), paramName: "name", type: "text" },
+ { label: t("Type"), paramName: "type", type: "select", options: uniq(inventories.map(i => i.type)) },
+ { label: t("Status"), paramName: "status", type: "select", options: uniq(inventories.map(i => i.status)) },
+ ], [t]
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredInventories(inventories)
+ }, [inventories])
+
+ const columns = useMemo[]>(
+ () => [
+ {
+ name: "code",
+ label: t("Code"),
+ },
+ {
+ name: "name",
+ label: t("Name"),
+ },
+ {
+ name: "type",
+ label: t("Type"),
+ },
+ {
+ name: "qty",
+ 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: ,
+ // unavailable: ,
+ // },
+ // colors: {
+ // available: "success",
+ // unavailable: "error",
+ // }
+ // },
+ ], [t]
+ )
+
+ return (
+ <>
+ {
+ console.log(query)
+ console.log(inventories)
+ setFilteredInventories(
+ inventories.filter(
+ (i) =>
+ i.code.toLowerCase().includes(query.code.toLowerCase()) &&
+ i.name.toLowerCase().includes(query.name.toLowerCase()) &&
+ (query.type == "All" || i.type.toLowerCase().includes(query.type.toLowerCase())) &&
+ (query.status == "All" || i.status.toLowerCase().includes(query.status.toLowerCase()))
+ )
+ )
+ }}
+ onReset={onReset}
+ />
+ items={filteredInventories} columns={columns} pagingController={{
+ pageNum: 0,
+ pageSize: 0,
+ totalCount: 0,
+ }} />
+ >
+ )
+}
+
+export default InventorySearch
\ No newline at end of file
diff --git a/src/components/InventorySearch/InventorySearchWrapper.tsx b/src/components/InventorySearch/InventorySearchWrapper.tsx
new file mode 100644
index 0000000..f207c7f
--- /dev/null
+++ b/src/components/InventorySearch/InventorySearchWrapper.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import GeneralLoading from "../General/GeneralLoading"
+import { fetchInventories } from "@/app/api/inventory";
+import InventorySearch from "./InventorySearch";
+
+interface SubComponents {
+ Loading: typeof GeneralLoading;
+}
+
+const InventorySearchWrapper: React.FC & SubComponents = async () => {
+
+ const [
+ inventories
+ ] = await Promise.all([
+ fetchInventories()
+ ])
+
+ return
+}
+
+
+InventorySearchWrapper.Loading = GeneralLoading;
+
+export default InventorySearchWrapper
\ No newline at end of file
diff --git a/src/components/InventorySearch/index.ts b/src/components/InventorySearch/index.ts
new file mode 100644
index 0000000..3d6573b
--- /dev/null
+++ b/src/components/InventorySearch/index.ts
@@ -0,0 +1 @@
+export { default } from "./InventorySearchWrapper"
\ No newline at end of file
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index c91fab8..71100c7 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -76,8 +76,8 @@ const NavigationContent: React.FC = () => {
},
{
icon: ,
- label: "View item In-out And invertory Ledger",
- path: "",
+ label: "View item In-out And inventory Ledger",
+ path: "/inventory",
},
],
},
diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx
index 560c6f9..bbd7c41 100644
--- a/src/components/SearchBox/SearchBox.tsx
+++ b/src/components/SearchBox/SearchBox.tsx
@@ -28,7 +28,7 @@ interface BaseCriterion {
label2?: string;
paramName: T;
paramName2?: T;
- options?: T[];
+ options?: T[] | string[];
filterObj?: T;
handleSelectionChange?: (selectedOptions: T[]) => void;
}
@@ -39,11 +39,11 @@ interface TextCriterion extends BaseCriterion {
interface SelectCriterion extends BaseCriterion {
type: "select";
- options: T[];
+ options: string[];
}
interface MultiSelectCriterion extends BaseCriterion {
- type: "select";
+ type: "multi-select";
options: T[];
selectedOptions: T[];
handleSelectionChange: (selectedOptions: T[]) => void;
diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx
index 75cdc78..6a5a863 100644
--- a/src/components/SearchResults/SearchResults.tsx
+++ b/src/components/SearchResults/SearchResults.tsx
@@ -4,32 +4,62 @@ import React from "react";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
-import TableCell from "@mui/material/TableCell";
+import TableCell, { TableCellProps } from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TablePagination, {
TablePaginationProps,
} from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow";
-import IconButton, {IconButtonOwnProps} from "@mui/material/IconButton";
+import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton";
+import { ButtonOwnProps, Icon, IconOwnProps, SxProps, Theme } from "@mui/material";
+import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
+import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
export interface ResultWithId {
id: string | number;
}
+type ColumnType = "icon" | "decimal" | "integer";
+
interface BaseColumn {
name: keyof T;
label: string;
+ align?: TableCellProps["align"];
+ headerAlign?: TableCellProps["align"];
+ sx?: SxProps | undefined;
+ style?: Partial & { [propName: string]: string };
+ type?: ColumnType;
+}
+
+interface IconColumn extends BaseColumn {
+ name: keyof T;
+ type: "icon";
+ icon?: React.ReactNode;
+ icons?: { [columnValue in keyof T]: React.ReactNode };
+ color?: IconOwnProps["color"];
+ colors?: { [columnValue in keyof T]: IconOwnProps["color"] };
+}
+
+interface DecimalColumn extends BaseColumn {
+ type: "decimal";
+}
+
+interface IntegerColumn extends BaseColumn {
+ type: "integer";
}
interface ColumnWithAction extends BaseColumn {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
+ buttonIcons: { [columnValue in keyof T]: React.ReactNode };
buttonColor?: IconButtonOwnProps["color"];
}
export type Column =
| BaseColumn
+ | IconColumn
+ | DecimalColumn
| ColumnWithAction;
interface Props {
@@ -42,7 +72,7 @@ interface Props {
totalCount: number
}) | { pageNum: number; pageSize: number; totalCount: number })) => void,
pagingController: { pageNum: number; pageSize: number; totalCount: number },
- isAutoPaging: boolean
+ isAutoPaging?: boolean
}
function isActionColumn(
@@ -51,14 +81,67 @@ function isActionColumn(
return Boolean((column as ColumnWithAction).onClick);
}
+function isIconColumn(
+ column: Column,
+): column is IconColumn {
+ return column.type === "icon";
+}
+
+function isDecimalColumn(
+ column: Column,
+): column is DecimalColumn {
+ return column.type === "decimal";
+}
+
+function isIntegerColumn(
+ column: Column,
+): column is IntegerColumn {
+ return column.type === "integer";
+}
+
+// Icon Component Functions
+function convertObjectKeysToLowercase(obj: T): object | undefined {
+ return obj ? Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])
+ ) : undefined;
+}
+
+function handleIconColors(
+ column: IconColumn,
+ value: T[keyof T],
+): IconOwnProps["color"] {
+ const colors = convertObjectKeysToLowercase(column.colors ?? {});
+ const valueKey = String(value).toLowerCase() as keyof typeof colors;
+
+ if (colors && valueKey in colors) {
+ return colors[valueKey];
+ }
+
+ return column.color ?? "primary";
+};
+
+function handleIconIcons(
+ column: IconColumn,
+ value: T[keyof T],
+): React.ReactNode {
+ const icons = convertObjectKeysToLowercase(column.icons ?? {});
+ const valueKey = String(value).toLowerCase() as keyof typeof icons;
+
+ if (icons && valueKey in icons) {
+ return icons[valueKey];
+ }
+
+ return column.icon ?? ;
+};
+
function SearchResults({
- items,
- columns,
- noWrapper,
- pagingController,
- setPagingController,
- isAutoPaging = true,
- }: Props) {
+ items,
+ columns,
+ noWrapper,
+ pagingController,
+ setPagingController,
+ isAutoPaging = true,
+}: Props) {
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
@@ -90,12 +173,12 @@ function SearchResults({
const table = (
<>
-
+
{columns.map((column, idx) => (
-
+
{column.label}
))}
@@ -113,18 +196,7 @@ function SearchResults({
const columnName = column.name;
return (
-
- {isActionColumn(column) ? (
- column.onClick(item)}
- >
- {column.buttonIcon}
-
- ) : (
- <>{item[columnName] as string}>
- )}
-
+
);
})}
@@ -139,19 +211,8 @@ function SearchResults({
const columnName = column.name;
return (
-
- {isActionColumn(column) ? (
- column.onClick(item)}
- >
- {column.buttonIcon}
-
- ) : (
- <>{item[columnName] as string}>
- )}
-
- );
+
+ );
})}
);
@@ -172,7 +233,49 @@ function SearchResults({
>
);
- return noWrapper ? table : {table};
+ return noWrapper ? table : {table};
+}
+
+// Table cells
+interface TableCellsProps {
+ column: Column,
+ columnName: keyof T,
+ idx: number,
+ item: T,
+}
+
+function TabelCells({
+ column,
+ columnName,
+ idx,
+ item
+}: TableCellsProps) {
+ return (
+
+ {isActionColumn(column) ? (
+ column.onClick(item)}
+ >
+ {column.buttonIcon}
+
+ ) :
+ isIconColumn(column) ? (
+
+ {handleIconIcons(column, item[columnName])}
+
+ ) :
+ isDecimalColumn(column) ? (
+ <>{decimalFormatter.format(Number(item[columnName]))}>
+ ) :
+ isIntegerColumn(column) ? (
+ <>{integerFormatter.format(Number(item[columnName]))}>
+ ) : (
+ <>{item[columnName] as string}>
+ )}
+ )
}
export default SearchResults;