@@ -6,6 +6,7 @@ import { cache } from "react"; | |||||
import { PoResult, StockInLine } from "."; | import { PoResult, StockInLine } from "."; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
import { QcItemResult } from "../settings/qcItem"; | import { QcItemResult } from "../settings/qcItem"; | ||||
import { RecordsRes } from "../utils"; | |||||
// import { BASE_API_URL } from "@/config/api"; | // import { BASE_API_URL } from "@/config/api"; | ||||
export interface PostStockInLiineResponse<T> { | export interface PostStockInLiineResponse<T> { | ||||
@@ -124,4 +125,18 @@ export const fetchPoInClient = cache(async (id: number) => { | |||||
}); | }); | ||||
}); | }); | ||||
export const fetchPoListClient = cache(async (queryParams?: Record<string, any>) => { | |||||
if (queryParams) { | |||||
const queryString = new URLSearchParams(queryParams).toString(); | |||||
return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list?${queryString}`, { | |||||
method: 'GET', | |||||
next: { tags: ["po"] }, | |||||
}); | |||||
} else { | |||||
return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list`, { | |||||
method: 'GET', | |||||
next: { tags: ["po"] }, | |||||
}); | |||||
} | |||||
}); | |||||
@@ -3,6 +3,7 @@ import "server-only"; | |||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { Uom } from "../settings/uom"; | import { Uom } from "../settings/uom"; | ||||
import { RecordsRes } from "../utils"; | |||||
export interface PoResult { | export interface PoResult { | ||||
id: number | id: number | ||||
@@ -55,10 +56,20 @@ export interface StockInLine { | |||||
defaultWarehouseId: number // id for now | defaultWarehouseId: number // id for now | ||||
} | } | ||||
export const fetchPoList = cache(async () => { | |||||
return serverFetchJson<PoResult[]>(`${BASE_API_URL}/po/list`, { | |||||
next: { tags: ["po"] }, | |||||
}); | |||||
export const fetchPoList = cache(async (queryParams?: Record<string, any>) => { | |||||
if (queryParams) { | |||||
const queryString = new URLSearchParams(queryParams).toString(); | |||||
return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list?${queryString}`, { | |||||
method: 'GET', | |||||
next: { tags: ["po"] }, | |||||
}); | |||||
} else { | |||||
return serverFetchJson<RecordsRes<PoResult[]>>(`${BASE_API_URL}/po/list`, { | |||||
method: 'GET', | |||||
next: { tags: ["po"] }, | |||||
}); | |||||
} | |||||
}); | }); | ||||
export const fetchPoWithStockInLines = cache(async (id: number) => { | export const fetchPoWithStockInLines = cache(async (id: number) => { | ||||
@@ -4,4 +4,9 @@ export interface CreateItemResponse<T> { | |||||
code: string; | code: string; | ||||
message: string | null; | message: string | null; | ||||
errorPosition: string | keyof T; | errorPosition: string | keyof T; | ||||
} | |||||
export interface RecordsRes<T>{ | |||||
records: T | |||||
total: number | |||||
} | } |
@@ -1,8 +1,8 @@ | |||||
"use client"; | "use client"; | ||||
import {useCallback, useEffect, useMemo, useState} from "react"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
import { ItemsResult} from "@/app/api/settings/item"; | |||||
import { ItemsResult } from "@/app/api/settings/item"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
@@ -10,7 +10,7 @@ import { useRouter, useSearchParams } from "next/navigation"; | |||||
import { GridDeleteIcon } from "@mui/x-data-grid"; | import { GridDeleteIcon } from "@mui/x-data-grid"; | ||||
import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
import axios from "axios"; | import axios from "axios"; | ||||
import {BASE_API_URL, NEXT_PUBLIC_API_URL} from "@/config/api"; | |||||
import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api"; | |||||
import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
type Props = { | type Props = { | ||||
@@ -25,21 +25,18 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
const router = useRouter(); | const router = useRouter(); | ||||
const [filterObj, setFilterObj] = useState({}); | const [filterObj, setFilterObj] = useState({}); | ||||
const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
pageNum: 1, | |||||
pageSize: 10, | |||||
totalCount: 0, | |||||
}) | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
() => { | |||||
var searchCriteria: Criterion<SearchParamNames>[] = [ | |||||
{ label: t("Code"), paramName: "code", type: "text" }, | |||||
{ label: t("Name"), paramName: "name", type: "text" }, | |||||
] | |||||
return searchCriteria | |||||
}, | |||||
[t, items] | |||||
); | |||||
pageNum: 1, | |||||
pageSize: 10, | |||||
totalCount: 0, | |||||
}); | |||||
const [totalCount, setTotalCount] = useState(0) | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | |||||
var searchCriteria: Criterion<SearchParamNames>[] = [ | |||||
{ label: t("Code"), paramName: "code", type: "text" }, | |||||
{ label: t("Name"), paramName: "name", type: "text" }, | |||||
]; | |||||
return searchCriteria; | |||||
}, [t, items]); | |||||
const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
(item: ItemsResult) => { | (item: ItemsResult) => { | ||||
@@ -48,10 +45,7 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
[router] | [router] | ||||
); | ); | ||||
const onDeleteClick = useCallback( | |||||
(item: ItemsResult) => {}, | |||||
[router] | |||||
); | |||||
const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); | |||||
const columns = useMemo<Column<ItemsResult>[]>( | const columns = useMemo<Column<ItemsResult>[]>( | ||||
() => [ | () => [ | ||||
@@ -79,41 +73,45 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
[filteredItems] | [filteredItems] | ||||
); | ); | ||||
useEffect(() => { | |||||
refetchData(filterObj); | |||||
}, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
const refetchData = async (filterObj: SearchQuery) => { | |||||
const authHeader = axiosInstance.defaults.headers['Authorization']; | |||||
if (!authHeader) { | |||||
return; // Exit the function if the token is not set | |||||
} | |||||
const params ={ | |||||
pageNum: pagingController.pageNum, | |||||
pageSize: pagingController.pageSize, | |||||
...filterObj, | |||||
const refetchData = useCallback( | |||||
async (filterObj: SearchQuery) => { | |||||
const authHeader = axiosInstance.defaults.headers["Authorization"]; | |||||
if (!authHeader) { | |||||
return; // Exit the function if the token is not set | |||||
} | |||||
const params = { | |||||
pageNum: pagingController.pageNum, | |||||
pageSize: pagingController.pageSize, | |||||
...filterObj, | |||||
}; | |||||
try { | |||||
const response = await axiosInstance.get<ItemsResult[]>( | |||||
`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, | |||||
{ params } | |||||
); | |||||
console.log(response); | |||||
if (response.status == 200) { | |||||
setFilteredItems(response.data.records); | |||||
setTotalCount(response.data.total) | |||||
return response; // Return the data from the response | |||||
} else { | |||||
throw "400"; | |||||
} | } | ||||
} catch (error) { | |||||
console.error("Error fetching items:", error); | |||||
throw error; // Rethrow the error for further handling | |||||
} | |||||
}, | |||||
[axiosInstance, pagingController.pageNum, pagingController.pageSize] | |||||
); | |||||
try { | |||||
const response = await axiosInstance.get<ItemsResult[]>(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }); | |||||
setFilteredItems(response.data.records); | |||||
setPagingController({ | |||||
...pagingController, | |||||
totalCount: response.data.total | |||||
}) | |||||
return response; // Return the data from the response | |||||
} catch (error) { | |||||
console.error('Error fetching items:', error); | |||||
throw error; // Rethrow the error for further handling | |||||
} | |||||
}; | |||||
useEffect(() => { | |||||
refetchData(filterObj); | |||||
}, [filterObj, pagingController.pageNum, pagingController.pageSize]); | |||||
const onReset = useCallback(() => { | |||||
setFilteredItems(items); | |||||
}, [items]); | |||||
const onReset = useCallback(() => { | |||||
setFilteredItems(items); | |||||
}, [items]); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -128,19 +126,19 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
// ); | // ); | ||||
// }) | // }) | ||||
// ); | // ); | ||||
// @ts-ignore | |||||
setFilterObj({ | |||||
...query | |||||
}) | |||||
setFilterObj({ | |||||
...query, | |||||
}); | |||||
}} | }} | ||||
onReset={onReset} | onReset={onReset} | ||||
/> | /> | ||||
<SearchResults<ItemsResult> | <SearchResults<ItemsResult> | ||||
items={filteredItems} | |||||
columns={columns} | |||||
setPagingController={setPagingController} | |||||
pagingController={pagingController} | |||||
isAutoPaging={false} | |||||
items={filteredItems} | |||||
columns={columns} | |||||
setPagingController={setPagingController} | |||||
pagingController={pagingController} | |||||
totalCount={totalCount} | |||||
isAutoPaging={false} | |||||
/> | /> | ||||
</> | </> | ||||
); | ); | ||||
@@ -17,8 +17,8 @@ const ItemsSearchWrapper: React.FC<Props> & SubComponents = async ({ | |||||
// type, | // type, | ||||
}) => { | }) => { | ||||
// console.log(type) | // console.log(type) | ||||
var result = await fetchAllItems() | |||||
return <ItemsSearch items={result} />; | |||||
// var result = await fetchAllItems() | |||||
return <ItemsSearch items={[]} />; | |||||
}; | }; | ||||
ItemsSearchWrapper.Loading = ItemsSearchLoading; | ItemsSearchWrapper.Loading = ItemsSearchLoading; | ||||
@@ -344,7 +344,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
{/* tab 1 */} | {/* tab 1 */} | ||||
<Grid sx={{ display: tabIndex === 0 ? "block" : "none" }}> | <Grid sx={{ display: tabIndex === 0 ? "block" : "none" }}> | ||||
<TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
<Table aria-label="collapsible table"> | |||||
<Table aria-label="collapsible table" stickyHeader> | |||||
<TableHead> | <TableHead> | ||||
<TableRow> | <TableRow> | ||||
<TableCell /> {/* for the collapse button */} | <TableCell /> {/* for the collapse button */} | ||||
@@ -1,7 +1,7 @@ | |||||
"use client"; | "use client"; | ||||
import { PoResult } from "@/app/api/po"; | import { PoResult } from "@/app/api/po"; | ||||
import { useCallback, useMemo, useState } from "react"; | |||||
import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
@@ -12,19 +12,25 @@ import QrModal from "../PoDetail/QrModal"; | |||||
import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
import NotificationIcon from '@mui/icons-material/NotificationImportant'; | import NotificationIcon from '@mui/icons-material/NotificationImportant'; | ||||
import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
import { defaultPagingController } from "../SearchResults/SearchResults"; | |||||
import { fetchPoListClient } from "@/app/api/po/actions"; | |||||
type Props = { | type Props = { | ||||
po: PoResult[]; | po: PoResult[]; | ||||
warehouse: WarehouseResult[]; | warehouse: WarehouseResult[]; | ||||
totalCount: number; | |||||
}; | }; | ||||
type SearchQuery = Partial<Omit<PoResult, "id">>; | type SearchQuery = Partial<Omit<PoResult, "id">>; | ||||
type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
const PoSearch: React.FC<Props> = ({ po, warehouse }) => { | |||||
// cal offset (pageSize) | |||||
// cal limit (pageSize) | |||||
const PoSearch: React.FC<Props> = ({ po, warehouse, totalCount: initTotalCount }) => { | |||||
const [filteredPo, setFilteredPo] = useState<PoResult[]>(po); | const [filteredPo, setFilteredPo] = useState<PoResult[]>(po); | ||||
const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
const [pagingController, setPagingController] = useState(defaultPagingController) | |||||
const [totalCount, setTotalCount] = useState(initTotalCount) | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | ||||
var searchCriteria: Criterion<SearchParamNames>[] = [ | var searchCriteria: Criterion<SearchParamNames>[] = [ | ||||
{ label: t("Code"), paramName: "code", type: "text" }, | { label: t("Code"), paramName: "code", type: "text" }, | ||||
@@ -102,6 +108,17 @@ const PoSearch: React.FC<Props> = ({ po, warehouse }) => { | |||||
setOpenScanner(false); | setOpenScanner(false); | ||||
}, []); | }, []); | ||||
const newPageFetch = useCallback(async (pagingController: Record<string, number>) => { | |||||
const res = await fetchPoListClient(pagingController) | |||||
if (res) { | |||||
setFilteredPo(res.records) | |||||
setTotalCount(res.total) | |||||
} | |||||
}, [fetchPoListClient, pagingController]) | |||||
useEffect(() => { | |||||
newPageFetch(pagingController) | |||||
}, [newPageFetch, pagingController]) | |||||
return ( | return ( | ||||
<> | <> | ||||
<Grid container> | <Grid container> | ||||
@@ -129,8 +146,8 @@ const PoSearch: React.FC<Props> = ({ po, warehouse }) => { | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
setFilteredPo( | |||||
po.filter((p) => { | |||||
setFilteredPo((prev) => | |||||
prev.filter((p) => { | |||||
return ( | return ( | ||||
p.code.toLowerCase().includes(query.code.toLowerCase()) && | p.code.toLowerCase().includes(query.code.toLowerCase()) && | ||||
(query.status === "All" || p.status === query.status) && | (query.status === "All" || p.status === query.status) && | ||||
@@ -141,7 +158,14 @@ const PoSearch: React.FC<Props> = ({ po, warehouse }) => { | |||||
}} | }} | ||||
onReset={onReset} | onReset={onReset} | ||||
/> | /> | ||||
<SearchResults<PoResult> items={filteredPo} columns={columns} /> | |||||
<SearchResults<PoResult> | |||||
items={filteredPo} | |||||
columns={columns} | |||||
pagingController={pagingController} | |||||
setPagingController={setPagingController} | |||||
totalCount={totalCount} | |||||
isAutoPaging={false} | |||||
/> | |||||
</> | </> | ||||
</> | </> | ||||
@@ -11,6 +11,7 @@ import dayjs from "dayjs"; | |||||
import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
import { fetchWarehouseList } from "@/app/api/warehouse"; | import { fetchWarehouseList } from "@/app/api/warehouse"; | ||||
import { defaultPagingController } from "../SearchResults/SearchResults"; | |||||
dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
interface SubComponents { | interface SubComponents { | ||||
@@ -26,21 +27,25 @@ const PoSearchWrapper: React.FC<Props> & SubComponents = async ( | |||||
// type, | // type, | ||||
} | } | ||||
) => { | ) => { | ||||
// console.log(defaultPagingController) | |||||
const [ | const [ | ||||
po, | po, | ||||
warehouse, | warehouse, | ||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchPoList(), | |||||
fetchPoList({ | |||||
"pageNum": 1, | |||||
"pageSize": 10, | |||||
}), | |||||
fetchWarehouseList(), | fetchWarehouseList(), | ||||
]); | ]); | ||||
console.log(po) | |||||
const fixPoDate = po.map((p) => { | |||||
console.log(po.records.length) | |||||
const fixPoDate = po.records.map((p) => { | |||||
return ({ | return ({ | ||||
...p, | ...p, | ||||
orderDate: dayjs(p.orderDate).add(-1, "month").format(OUTPUT_DATE_FORMAT) | orderDate: dayjs(p.orderDate).add(-1, "month").format(OUTPUT_DATE_FORMAT) | ||||
}) | }) | ||||
}) | }) | ||||
return <PoSearch po={fixPoDate} warehouse={warehouse}/>; | |||||
return <PoSearch po={fixPoDate} warehouse={warehouse} totalCount={po.total}/>; | |||||
}; | }; | ||||
PoSearchWrapper.Loading = PoSearchLoading; | PoSearchWrapper.Loading = PoSearchLoading; | ||||
@@ -1,6 +1,6 @@ | |||||
"use client"; | "use client"; | ||||
import React from "react"; | |||||
import React, { Dispatch, SetStateAction } from "react"; | |||||
import Paper from "@mui/material/Paper"; | import Paper from "@mui/material/Paper"; | ||||
import Table from "@mui/material/Table"; | import Table from "@mui/material/Table"; | ||||
import TableBody from "@mui/material/TableBody"; | import TableBody from "@mui/material/TableBody"; | ||||
@@ -8,279 +8,314 @@ import TableCell, { TableCellProps } from "@mui/material/TableCell"; | |||||
import TableContainer from "@mui/material/TableContainer"; | import TableContainer from "@mui/material/TableContainer"; | ||||
import TableHead from "@mui/material/TableHead"; | import TableHead from "@mui/material/TableHead"; | ||||
import TablePagination, { | import TablePagination, { | ||||
TablePaginationProps, | |||||
TablePaginationProps, | |||||
} from "@mui/material/TablePagination"; | } from "@mui/material/TablePagination"; | ||||
import TableRow from "@mui/material/TableRow"; | 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 { | |||||
ButtonOwnProps, | |||||
Icon, | |||||
IconOwnProps, | |||||
SxProps, | |||||
Theme, | |||||
} from "@mui/material"; | |||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; | |||||
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | ||||
export interface ResultWithId { | export interface ResultWithId { | ||||
id: string | number; | |||||
id: string | number; | |||||
} | } | ||||
type ColumnType = "icon" | "decimal" | "integer"; | type ColumnType = "icon" | "decimal" | "integer"; | ||||
interface BaseColumn<T extends ResultWithId> { | interface BaseColumn<T extends ResultWithId> { | ||||
name: keyof T; | |||||
label: string; | |||||
align?: TableCellProps["align"]; | |||||
headerAlign?: TableCellProps["align"]; | |||||
sx?: SxProps<Theme> | undefined; | |||||
style?: Partial<HTMLElement["style"]> & { [propName: string]: string }; | |||||
type?: ColumnType; | |||||
renderCell?: (params: T) => React.ReactNode; | |||||
name: keyof T; | |||||
label: string; | |||||
align?: TableCellProps["align"]; | |||||
headerAlign?: TableCellProps["align"]; | |||||
sx?: SxProps<Theme> | undefined; | |||||
style?: Partial<HTMLElement["style"]> & { [propName: string]: string }; | |||||
type?: ColumnType; | |||||
renderCell?: (params: T) => React.ReactNode; | |||||
} | } | ||||
interface IconColumn<T extends ResultWithId> extends BaseColumn<T> { | interface IconColumn<T extends ResultWithId> extends BaseColumn<T> { | ||||
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"] }; | |||||
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<T extends ResultWithId> extends BaseColumn<T> { | interface DecimalColumn<T extends ResultWithId> extends BaseColumn<T> { | ||||
type: "decimal"; | |||||
type: "decimal"; | |||||
} | } | ||||
interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> { | interface IntegerColumn<T extends ResultWithId> extends BaseColumn<T> { | ||||
type: "integer"; | |||||
type: "integer"; | |||||
} | } | ||||
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { | ||||
onClick: (item: T) => void; | |||||
buttonIcon: React.ReactNode; | |||||
buttonIcons: { [columnValue in keyof T]: React.ReactNode }; | |||||
buttonColor?: IconButtonOwnProps["color"]; | |||||
onClick: (item: T) => void; | |||||
buttonIcon: React.ReactNode; | |||||
buttonIcons: { [columnValue in keyof T]: React.ReactNode }; | |||||
buttonColor?: IconButtonOwnProps["color"]; | |||||
} | } | ||||
export type Column<T extends ResultWithId> = | export type Column<T extends ResultWithId> = | ||||
| BaseColumn<T> | |||||
| IconColumn<T> | |||||
| DecimalColumn<T> | |||||
| ColumnWithAction<T>; | |||||
| BaseColumn<T> | |||||
| IconColumn<T> | |||||
| DecimalColumn<T> | |||||
| ColumnWithAction<T>; | |||||
interface Props<T extends ResultWithId> { | interface Props<T extends ResultWithId> { | ||||
items: T[], | |||||
columns: Column<T>[], | |||||
noWrapper?: boolean, | |||||
setPagingController?: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => { | |||||
pageNum: number; | |||||
pageSize: number; | |||||
totalCount: number | |||||
}) | { pageNum: number; pageSize: number; totalCount: number })) => void, | |||||
pagingController: { pageNum: number; pageSize: number; totalCount: number }, | |||||
isAutoPaging?: boolean | |||||
totalCount?: number; | |||||
items: T[]; | |||||
columns: Column<T>[]; | |||||
noWrapper?: boolean; | |||||
setPagingController?: Dispatch<SetStateAction<{ | |||||
pageNum: number; | |||||
pageSize: number; | |||||
}>> | |||||
pagingController: { pageNum: number; pageSize: number;}; | |||||
isAutoPaging?: boolean; | |||||
} | } | ||||
function isActionColumn<T extends ResultWithId>( | function isActionColumn<T extends ResultWithId>( | ||||
column: Column<T>, | |||||
column: Column<T> | |||||
): column is ColumnWithAction<T> { | ): column is ColumnWithAction<T> { | ||||
return Boolean((column as ColumnWithAction<T>).onClick); | |||||
return Boolean((column as ColumnWithAction<T>).onClick); | |||||
} | } | ||||
function isIconColumn<T extends ResultWithId>( | function isIconColumn<T extends ResultWithId>( | ||||
column: Column<T>, | |||||
column: Column<T> | |||||
): column is IconColumn<T> { | ): column is IconColumn<T> { | ||||
return column.type === "icon"; | |||||
return column.type === "icon"; | |||||
} | } | ||||
function isDecimalColumn<T extends ResultWithId>( | function isDecimalColumn<T extends ResultWithId>( | ||||
column: Column<T>, | |||||
column: Column<T> | |||||
): column is DecimalColumn<T> { | ): column is DecimalColumn<T> { | ||||
return column.type === "decimal"; | |||||
return column.type === "decimal"; | |||||
} | } | ||||
function isIntegerColumn<T extends ResultWithId>( | function isIntegerColumn<T extends ResultWithId>( | ||||
column: Column<T>, | |||||
column: Column<T> | |||||
): column is IntegerColumn<T> { | ): column is IntegerColumn<T> { | ||||
return column.type === "integer"; | |||||
return column.type === "integer"; | |||||
} | } | ||||
// Icon Component Functions | // Icon Component Functions | ||||
function convertObjectKeysToLowercase<T extends object>(obj: T): object | undefined { | |||||
return obj ? Object.fromEntries( | |||||
function convertObjectKeysToLowercase<T extends object>( | |||||
obj: T | |||||
): 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; | |||||
) | |||||
: undefined; | |||||
} | } | ||||
function handleIconColors<T extends ResultWithId>( | function handleIconColors<T extends ResultWithId>( | ||||
column: IconColumn<T>, | |||||
value: T[keyof T], | |||||
column: IconColumn<T>, | |||||
value: T[keyof T] | |||||
): IconOwnProps["color"] { | ): IconOwnProps["color"] { | ||||
const colors = convertObjectKeysToLowercase(column.colors ?? {}); | |||||
const valueKey = String(value).toLowerCase() as keyof typeof colors; | |||||
const colors = convertObjectKeysToLowercase(column.colors ?? {}); | |||||
const valueKey = String(value).toLowerCase() as keyof typeof colors; | |||||
if (colors && valueKey in colors) { | |||||
return colors[valueKey]; | |||||
} | |||||
if (colors && valueKey in colors) { | |||||
return colors[valueKey]; | |||||
} | |||||
return column.color ?? "primary"; | |||||
}; | |||||
return column.color ?? "primary"; | |||||
} | |||||
function handleIconIcons<T extends ResultWithId>( | function handleIconIcons<T extends ResultWithId>( | ||||
column: IconColumn<T>, | |||||
value: T[keyof T], | |||||
column: IconColumn<T>, | |||||
value: T[keyof T] | |||||
): React.ReactNode { | ): React.ReactNode { | ||||
const icons = convertObjectKeysToLowercase(column.icons ?? {}); | |||||
const valueKey = String(value).toLowerCase() as keyof typeof icons; | |||||
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 ?? <CheckCircleOutlineIcon fontSize="small" />; | |||||
}; | |||||
if (icons && valueKey in icons) { | |||||
return icons[valueKey]; | |||||
} | |||||
return column.icon ?? <CheckCircleOutlineIcon fontSize="small" />; | |||||
} | |||||
export const defaultPagingController:{ pageNum: number; pageSize: number} = { | |||||
"pageNum": 1, | |||||
"pageSize": 10, | |||||
} | |||||
function SearchResults<T extends ResultWithId>({ | function SearchResults<T extends ResultWithId>({ | ||||
items, | |||||
columns, | |||||
noWrapper, | |||||
pagingController, | |||||
setPagingController, | |||||
isAutoPaging = true, | |||||
items, | |||||
columns, | |||||
noWrapper, | |||||
pagingController, | |||||
setPagingController, | |||||
isAutoPaging = true, | |||||
totalCount | |||||
}: Props<T>) { | }: Props<T>) { | ||||
const [page, setPage] = React.useState(0); | |||||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||||
const [page, setPage] = React.useState(0); | |||||
const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||||
/// this | |||||
const handleChangePage: TablePaginationProps["onPageChange"] = ( | |||||
_event, | |||||
newPage, | |||||
) => { | |||||
console.log(_event) | |||||
setPage(newPage); | |||||
if (setPagingController) { | |||||
setPagingController({ | |||||
...pagingController, | |||||
pageNum: newPage + 1, | |||||
}) | |||||
} | |||||
/// this | |||||
const handleChangePage: TablePaginationProps["onPageChange"] = ( | |||||
_event, | |||||
newPage | |||||
) => { | |||||
console.log(_event); | |||||
setPage(newPage); | |||||
if (setPagingController) { | |||||
setPagingController({ | |||||
...pagingController, | |||||
pageNum: newPage + 1, | |||||
}); | |||||
} | } | ||||
}; | |||||
const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | |||||
event, | |||||
) => { | |||||
console.log(event) | |||||
setRowsPerPage(+event.target.value); | |||||
setPage(0); | |||||
if (setPagingController) { | |||||
setPagingController({ | |||||
...pagingController, | |||||
pageNum: +event.target.value, | |||||
}) | |||||
} | |||||
}; | |||||
const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | |||||
event | |||||
) => { | |||||
console.log(event); | |||||
setRowsPerPage(+event.target.value); | |||||
setPage(0); | |||||
if (setPagingController) { | |||||
setPagingController({ | |||||
...pagingController, | |||||
pageNum: +event.target.value, | |||||
}); | |||||
} | |||||
}; | |||||
const table = ( | |||||
<> | |||||
<TableContainer sx={{ maxHeight: 440 }}> | |||||
<Table stickyHeader> | |||||
<TableHead> | |||||
<TableRow> | |||||
{columns.map((column, idx) => ( | |||||
<TableCell align={column.headerAlign} sx={column.sx} key={`${column.name.toString()}${idx}`}> | |||||
{column.label} | |||||
</TableCell> | |||||
))} | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{ | |||||
isAutoPaging ? | |||||
items | |||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||||
.map((item) => { | |||||
return ( | |||||
<TableRow hover tabIndex={-1} key={item.id}> | |||||
{columns.map((column, idx) => { | |||||
const columnName = column.name; | |||||
const table = ( | |||||
<> | |||||
<TableContainer sx={{ maxHeight: 440 }}> | |||||
<Table stickyHeader> | |||||
<TableHead> | |||||
<TableRow> | |||||
{columns.map((column, idx) => ( | |||||
<TableCell | |||||
align={column.headerAlign} | |||||
sx={column.sx} | |||||
key={`${column.name.toString()}${idx}`} | |||||
> | |||||
{column.label} | |||||
</TableCell> | |||||
))} | |||||
</TableRow> | |||||
</TableHead> | |||||
<TableBody> | |||||
{isAutoPaging | |||||
? items | |||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||||
.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}/> | |||||
); | |||||
})} | |||||
</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} | |||||
/> | |||||
); | |||||
})} | |||||
</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}/> | |||||
return ( | |||||
<TabelCells | |||||
key={`${columnName.toString()}-${idx}`} | |||||
column={column} | |||||
columnName={columnName} | |||||
idx={idx} | |||||
item={item} | |||||
/> | |||||
); | ); | ||||
})} | |||||
</TableRow> | |||||
); | |||||
}) | |||||
} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
<TablePagination | |||||
rowsPerPageOptions={[10, 25, 100]} | |||||
component="div" | |||||
count={!pagingController || pagingController.totalCount == 0 ? items.length : pagingController.totalCount} | |||||
rowsPerPage={rowsPerPage} | |||||
page={page} | |||||
onPageChange={handleChangePage} | |||||
onRowsPerPageChange={handleChangeRowsPerPage} | |||||
/> | |||||
</> | |||||
); | |||||
})} | |||||
</TableRow> | |||||
); | |||||
})} | |||||
</TableBody> | |||||
</Table> | |||||
</TableContainer> | |||||
<TablePagination | |||||
rowsPerPageOptions={[10, 25, 100]} | |||||
component="div" | |||||
count={!totalCount || totalCount == 0 | |||||
? items.length | |||||
: totalCount | |||||
} | |||||
// count={ | |||||
// !pagingController || pagingController.totalCount == 0 | |||||
// ? items.length | |||||
// : pagingController.totalCount | |||||
// } | |||||
rowsPerPage={rowsPerPage} | |||||
page={page} | |||||
onPageChange={handleChangePage} | |||||
onRowsPerPageChange={handleChangeRowsPerPage} | |||||
/> | |||||
</> | |||||
); | |||||
return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||||
return noWrapper ? table : <Paper sx={{ overflow: "hidden" }}>{table}</Paper>; | |||||
} | } | ||||
// Table cells | // Table cells | ||||
interface TableCellsProps<T extends ResultWithId> { | interface TableCellsProps<T extends ResultWithId> { | ||||
column: Column<T>, | |||||
columnName: keyof T, | |||||
idx: number, | |||||
item: T, | |||||
column: Column<T>; | |||||
columnName: keyof T; | |||||
idx: number; | |||||
item: T; | |||||
} | } | ||||
function TabelCells<T extends ResultWithId>({ | function TabelCells<T extends ResultWithId>({ | ||||
column, | |||||
columnName, | |||||
idx, | |||||
item | |||||
column, | |||||
columnName, | |||||
idx, | |||||
item, | |||||
}: TableCellsProps<T>) { | }: TableCellsProps<T>) { | ||||
return ( | |||||
<TableCell align={column.align} sx={column.sx} key={`${columnName.toString()}-${idx}`}> | |||||
{isActionColumn(column) ? ( | |||||
<IconButton | |||||
color={column.buttonColor ?? "primary"} | |||||
onClick={() => column.onClick(item)} | |||||
> | |||||
{column.buttonIcon} | |||||
</IconButton> | |||||
) : | |||||
isIconColumn(column) ? ( | |||||
<Icon | |||||
color={handleIconColors(column, item[columnName])} | |||||
> | |||||
{handleIconIcons(column, item[columnName])} | |||||
</Icon> | |||||
) : | |||||
isDecimalColumn(column) ? ( | |||||
<>{decimalFormatter.format(Number(item[columnName]))}</> | |||||
) : | |||||
isIntegerColumn(column) ? ( | |||||
<>{integerFormatter.format(Number(item[columnName]))}</> | |||||
) : | |||||
( | |||||
column.renderCell ? column.renderCell(item) : <>{item[columnName] as string}</> | |||||
)} | |||||
</TableCell>) | |||||
return ( | |||||
<TableCell | |||||
align={column.align} | |||||
sx={column.sx} | |||||
key={`${columnName.toString()}-${idx}`} | |||||
> | |||||
{isActionColumn(column) ? ( | |||||
<IconButton | |||||
color={column.buttonColor ?? "primary"} | |||||
onClick={() => column.onClick(item)} | |||||
> | |||||
{column.buttonIcon} | |||||
</IconButton> | |||||
) : isIconColumn(column) ? ( | |||||
<Icon color={handleIconColors(column, item[columnName])}> | |||||
{handleIconIcons(column, item[columnName])} | |||||
</Icon> | |||||
) : isDecimalColumn(column) ? ( | |||||
<>{decimalFormatter.format(Number(item[columnName]))}</> | |||||
) : isIntegerColumn(column) ? ( | |||||
<>{integerFormatter.format(Number(item[columnName]))}</> | |||||
) : column.renderCell ? ( | |||||
column.renderCell(item) | |||||
) : ( | |||||
<>{item[columnName] as string}</> | |||||
)} | |||||
</TableCell> | |||||
); | |||||
} | } | ||||
export default SearchResults; | export default SearchResults; |