From 09638ea0a6c549c0a0e930f5d6be29b5e5cd26d7 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 16 Jun 2025 14:22:37 +0800 Subject: [PATCH] update --- src/app/api/po/actions.ts | 15 + src/app/api/po/index.ts | 19 +- src/app/api/utils/index.ts | 5 + src/components/ItemsSearch/ItemsSearch.tsx | 124 +++-- .../ItemsSearch/ItemsSearchWrapper.tsx | 4 +- src/components/PoDetail/PoDetail.tsx | 2 +- src/components/PoSearch/PoSearch.tsx | 36 +- src/components/PoSearch/PoSearchWrapper.tsx | 13 +- .../SearchResults/SearchResults.tsx | 429 ++++++++++-------- 9 files changed, 370 insertions(+), 277 deletions(-) diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts index 762086e..6dab3f3 100644 --- a/src/app/api/po/actions.ts +++ b/src/app/api/po/actions.ts @@ -6,6 +6,7 @@ import { cache } from "react"; import { PoResult, StockInLine } from "."; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { QcItemResult } from "../settings/qcItem"; +import { RecordsRes } from "../utils"; // import { BASE_API_URL } from "@/config/api"; export interface PostStockInLiineResponse { @@ -124,4 +125,18 @@ export const fetchPoInClient = cache(async (id: number) => { }); }); + export const fetchPoListClient = cache(async (queryParams?: Record) => { + if (queryParams) { + const queryString = new URLSearchParams(queryParams).toString(); + return serverFetchJson>(`${BASE_API_URL}/po/list?${queryString}`, { + method: 'GET', + next: { tags: ["po"] }, + }); + } else { + return serverFetchJson>(`${BASE_API_URL}/po/list`, { + method: 'GET', + next: { tags: ["po"] }, + }); + } + }); diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts index ac50ec4..6cb64fa 100644 --- a/src/app/api/po/index.ts +++ b/src/app/api/po/index.ts @@ -3,6 +3,7 @@ import "server-only"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { Uom } from "../settings/uom"; +import { RecordsRes } from "../utils"; export interface PoResult { id: number @@ -55,10 +56,20 @@ export interface StockInLine { defaultWarehouseId: number // id for now } - export const fetchPoList = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/po/list`, { - next: { tags: ["po"] }, - }); + + export const fetchPoList = cache(async (queryParams?: Record) => { + if (queryParams) { + const queryString = new URLSearchParams(queryParams).toString(); + return serverFetchJson>(`${BASE_API_URL}/po/list?${queryString}`, { + method: 'GET', + next: { tags: ["po"] }, + }); + } else { + return serverFetchJson>(`${BASE_API_URL}/po/list`, { + method: 'GET', + next: { tags: ["po"] }, + }); + } }); export const fetchPoWithStockInLines = cache(async (id: number) => { diff --git a/src/app/api/utils/index.ts b/src/app/api/utils/index.ts index 2134176..346506f 100644 --- a/src/app/api/utils/index.ts +++ b/src/app/api/utils/index.ts @@ -4,4 +4,9 @@ export interface CreateItemResponse { code: string; message: string | null; errorPosition: string | keyof T; +} + +export interface RecordsRes{ + records: T + total: number } \ No newline at end of file diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index b8a3f6a..2d4cb76 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -1,8 +1,8 @@ "use client"; -import {useCallback, useEffect, useMemo, useState} from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; 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 SearchResults, { Column } from "../SearchResults"; import { EditNote } from "@mui/icons-material"; @@ -10,7 +10,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { GridDeleteIcon } from "@mui/x-data-grid"; import { TypeEnum } from "@/app/utils/typeEnum"; 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"; type Props = { @@ -25,21 +25,18 @@ const ItemsSearch: React.FC = ({ items }) => { const router = useRouter(); const [filterObj, setFilterObj] = useState({}); const [pagingController, setPagingController] = useState({ - pageNum: 1, - pageSize: 10, - totalCount: 0, - }) - - const searchCriteria: Criterion[] = useMemo( - () => { - var searchCriteria: Criterion[] = [ - { 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[] = useMemo(() => { + var searchCriteria: Criterion[] = [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ]; + return searchCriteria; + }, [t, items]); const onDetailClick = useCallback( (item: ItemsResult) => { @@ -48,10 +45,7 @@ const ItemsSearch: React.FC = ({ items }) => { [router] ); - const onDeleteClick = useCallback( - (item: ItemsResult) => {}, - [router] - ); + const onDeleteClick = useCallback((item: ItemsResult) => {}, [router]); const columns = useMemo[]>( () => [ @@ -79,41 +73,45 @@ const ItemsSearch: React.FC = ({ items }) => { [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( + `${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(`${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 ( <> @@ -128,19 +126,19 @@ const ItemsSearch: React.FC = ({ items }) => { // ); // }) // ); - // @ts-ignore - setFilterObj({ - ...query - }) + setFilterObj({ + ...query, + }); }} onReset={onReset} /> - items={filteredItems} - columns={columns} - setPagingController={setPagingController} - pagingController={pagingController} - isAutoPaging={false} + items={filteredItems} + columns={columns} + setPagingController={setPagingController} + pagingController={pagingController} + totalCount={totalCount} + isAutoPaging={false} /> ); diff --git a/src/components/ItemsSearch/ItemsSearchWrapper.tsx b/src/components/ItemsSearch/ItemsSearchWrapper.tsx index 59e8a65..fbfd01b 100644 --- a/src/components/ItemsSearch/ItemsSearchWrapper.tsx +++ b/src/components/ItemsSearch/ItemsSearchWrapper.tsx @@ -17,8 +17,8 @@ const ItemsSearchWrapper: React.FC & SubComponents = async ({ // type, }) => { // console.log(type) - var result = await fetchAllItems() - return ; + // var result = await fetchAllItems() + return ; }; ItemsSearchWrapper.Loading = ItemsSearchLoading; diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index a50df20..eba9ef7 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -344,7 +344,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { {/* tab 1 */} - +
{/* for the collapse button */} diff --git a/src/components/PoSearch/PoSearch.tsx b/src/components/PoSearch/PoSearch.tsx index f3812df..965f3b3 100644 --- a/src/components/PoSearch/PoSearch.tsx +++ b/src/components/PoSearch/PoSearch.tsx @@ -1,7 +1,7 @@ "use client"; 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 { useRouter, useSearchParams } from "next/navigation"; import SearchBox, { Criterion } from "../SearchBox"; @@ -12,19 +12,25 @@ import QrModal from "../PoDetail/QrModal"; import { WarehouseResult } from "@/app/api/warehouse"; import NotificationIcon from '@mui/icons-material/NotificationImportant'; import { useSession } from "next-auth/react"; +import { defaultPagingController } from "../SearchResults/SearchResults"; +import { fetchPoListClient } from "@/app/api/po/actions"; type Props = { po: PoResult[]; warehouse: WarehouseResult[]; + totalCount: number; }; type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const PoSearch: React.FC = ({ po, warehouse }) => { +// cal offset (pageSize) +// cal limit (pageSize) +const PoSearch: React.FC = ({ po, warehouse, totalCount: initTotalCount }) => { const [filteredPo, setFilteredPo] = useState(po); const { t } = useTranslation("purchaseOrder"); const router = useRouter(); - + const [pagingController, setPagingController] = useState(defaultPagingController) + const [totalCount, setTotalCount] = useState(initTotalCount) const searchCriteria: Criterion[] = useMemo(() => { var searchCriteria: Criterion[] = [ { label: t("Code"), paramName: "code", type: "text" }, @@ -102,6 +108,17 @@ const PoSearch: React.FC = ({ po, warehouse }) => { setOpenScanner(false); }, []); + const newPageFetch = useCallback(async (pagingController: Record) => { + const res = await fetchPoListClient(pagingController) + if (res) { + setFilteredPo(res.records) + setTotalCount(res.total) + } + }, [fetchPoListClient, pagingController]) + + useEffect(() => { + newPageFetch(pagingController) + }, [newPageFetch, pagingController]) return ( <> @@ -129,8 +146,8 @@ const PoSearch: React.FC = ({ po, warehouse }) => { { - setFilteredPo( - po.filter((p) => { + setFilteredPo((prev) => + prev.filter((p) => { return ( p.code.toLowerCase().includes(query.code.toLowerCase()) && (query.status === "All" || p.status === query.status) && @@ -141,7 +158,14 @@ const PoSearch: React.FC = ({ po, warehouse }) => { }} onReset={onReset} /> - items={filteredPo} columns={columns} /> + + items={filteredPo} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCount} + isAutoPaging={false} + /> diff --git a/src/components/PoSearch/PoSearchWrapper.tsx b/src/components/PoSearch/PoSearchWrapper.tsx index 6e6a957..a3c278a 100644 --- a/src/components/PoSearch/PoSearchWrapper.tsx +++ b/src/components/PoSearch/PoSearchWrapper.tsx @@ -11,6 +11,7 @@ import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { fetchWarehouseList } from "@/app/api/warehouse"; +import { defaultPagingController } from "../SearchResults/SearchResults"; dayjs.extend(arraySupport); interface SubComponents { @@ -26,21 +27,25 @@ const PoSearchWrapper: React.FC & SubComponents = async ( // type, } ) => { + // console.log(defaultPagingController) const [ po, warehouse, ] = await Promise.all([ - fetchPoList(), + fetchPoList({ + "pageNum": 1, + "pageSize": 10, + }), fetchWarehouseList(), ]); - console.log(po) - const fixPoDate = po.map((p) => { + console.log(po.records.length) + const fixPoDate = po.records.map((p) => { return ({ ...p, orderDate: dayjs(p.orderDate).add(-1, "month").format(OUTPUT_DATE_FORMAT) }) }) - return ; + return ; }; PoSearchWrapper.Loading = PoSearchLoading; diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index adca1cf..8d41007 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { Dispatch, SetStateAction } from "react"; import Paper from "@mui/material/Paper"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; @@ -8,279 +8,314 @@ import TableCell, { TableCellProps } from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TablePagination, { - TablePaginationProps, + TablePaginationProps, } from "@mui/material/TablePagination"; import TableRow from "@mui/material/TableRow"; 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"; export interface ResultWithId { - id: string | number; + 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; - renderCell?: (params: T) => React.ReactNode; + name: keyof T; + label: string; + align?: TableCellProps["align"]; + headerAlign?: TableCellProps["align"]; + sx?: SxProps | undefined; + style?: Partial & { [propName: string]: string }; + type?: ColumnType; + renderCell?: (params: T) => React.ReactNode; } 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"] }; + 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"; + type: "decimal"; } interface IntegerColumn extends BaseColumn { - type: "integer"; + type: "integer"; } interface ColumnWithAction extends BaseColumn { - 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 = - | BaseColumn - | IconColumn - | DecimalColumn - | ColumnWithAction; + | BaseColumn + | IconColumn + | DecimalColumn + | ColumnWithAction; interface Props { - items: T[], - columns: Column[], - 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[]; + noWrapper?: boolean; + setPagingController?: Dispatch> + pagingController: { pageNum: number; pageSize: number;}; + isAutoPaging?: boolean; } function isActionColumn( - column: Column, + column: Column ): column is ColumnWithAction { - return Boolean((column as ColumnWithAction).onClick); + return Boolean((column as ColumnWithAction).onClick); } function isIconColumn( - column: Column, + column: Column ): column is IconColumn { - return column.type === "icon"; + return column.type === "icon"; } function isDecimalColumn( - column: Column, + column: Column ): column is DecimalColumn { - return column.type === "decimal"; + return column.type === "decimal"; } function isIntegerColumn( - column: Column, + column: Column ): column is IntegerColumn { - return column.type === "integer"; + return column.type === "integer"; } // Icon Component Functions -function convertObjectKeysToLowercase(obj: T): object | undefined { - return obj ? Object.fromEntries( +function convertObjectKeysToLowercase( + obj: T +): object | undefined { + return obj + ? Object.fromEntries( Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]) - ) : undefined; + ) + : undefined; } function handleIconColors( - column: IconColumn, - value: T[keyof T], + column: IconColumn, + value: T[keyof T] ): 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( - column: IconColumn, - value: T[keyof T], + column: IconColumn, + value: T[keyof T] ): 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 ?? ; -}; + if (icons && valueKey in icons) { + return icons[valueKey]; + } + return column.icon ?? ; +} +export const defaultPagingController:{ pageNum: number; pageSize: number} = { + "pageNum": 1, + "pageSize": 10, +} function SearchResults({ - items, - columns, - noWrapper, - pagingController, - setPagingController, - isAutoPaging = true, + items, + columns, + noWrapper, + pagingController, + setPagingController, + isAutoPaging = true, + totalCount }: Props) { - 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 = ( - <> - -
- - - {columns.map((column, idx) => ( - - {column.label} - - ))} - - - - { - isAutoPaging ? - items - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((item) => { - return ( - - {columns.map((column, idx) => { - const columnName = column.name; + const table = ( + <> + +
+ + + {columns.map((column, idx) => ( + + {column.label} + + ))} + + + + {isAutoPaging + ? items + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((item) => { + return ( + + {columns.map((column, idx) => { + const columnName = column.name; - return ( - - ); - })} - - ); - }) - : - items - .map((item) => { - return ( - - {columns.map((column, idx) => { - const columnName = column.name; + return ( + + ); + })} + + ); + }) + : items.map((item) => { + return ( + + {columns.map((column, idx) => { + const columnName = column.name; - return ( - + return ( + ); - })} - - ); - }) - } - -
-
- - - ); + })} + + ); + })} + + + + + + ); - return noWrapper ? table : {table}; + return noWrapper ? table : {table}; } // Table cells interface TableCellsProps { - column: Column, - columnName: keyof T, - idx: number, - item: T, + column: Column; + columnName: keyof T; + idx: number; + item: T; } function TabelCells({ - column, - columnName, - idx, - item + 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]))} - ) : - ( - column.renderCell ? column.renderCell(item) : <>{item[columnName] as string} - )} - ) + 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]))} + ) : column.renderCell ? ( + column.renderCell(item) + ) : ( + <>{item[columnName] as string} + )} + + ); } export default SearchResults;