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