| @@ -0,0 +1,35 @@ | |||
| import JoSearch from "@/components/JoSearch"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Stack, Typography } from "@mui/material"; | |||
| import { Metadata } from "next"; | |||
| import React, { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Job Order" | |||
| } | |||
| const jo: React.FC = async () => { | |||
| const { t } = await getServerI18n("jo"); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Job Order")} | |||
| </Typography> | |||
| </Stack> | |||
| <I18nProvider namespaces={["jo", "common"]}> | |||
| <Suspense fallback={<JoSearch.Loading />}> | |||
| <JoSearch /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| ) | |||
| } | |||
| export default jo; | |||
| @@ -1,8 +1,28 @@ | |||
| "use server"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { Machine, Operator } from "."; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
| export interface SearchJoResultRequest extends Pageable { | |||
| code: string; | |||
| name: string; | |||
| } | |||
| export interface SearchJoResultResponse { | |||
| records: SearchJoResult[]; | |||
| total: number; | |||
| } | |||
| export interface SearchJoResult{ | |||
| id: number; | |||
| code: string; | |||
| name: string; | |||
| reqQty: number; | |||
| uom: string; | |||
| status: string; | |||
| } | |||
| export interface IsOperatorExistResponse<T> { | |||
| id: number | null; | |||
| @@ -49,3 +69,20 @@ export const isCorrectMachineUsed = async (machineCode: string) => { | |||
| revalidateTag("po"); | |||
| return isExist; | |||
| }; | |||
| export const fetchJos = async (data?: SearchJoResultRequest) => { | |||
| const queryStr = convertObjToURLSearchParams(data) | |||
| const response = await serverFetchJson<SearchJoResultResponse>( | |||
| `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | |||
| { | |||
| method: "GET", | |||
| headers: { "Content-Type": "application/json" }, | |||
| next: { | |||
| tags: ["jos"] | |||
| } | |||
| } | |||
| ) | |||
| return response | |||
| } | |||
| @@ -11,7 +11,7 @@ export const downloadFile = (blobData: Uint8Array, filename: string) => { | |||
| }; | |||
| export const convertObjToURLSearchParams = <T extends Object>( | |||
| data: T | null, | |||
| data?: T | null, | |||
| ): string => { | |||
| if (isEmpty(data)) { | |||
| return ""; | |||
| @@ -25,6 +25,8 @@ const pathToLabelMap: { [path: string]: string } = { | |||
| "/pickOrder": "Pick Order", | |||
| "/po": "Purchase Order", | |||
| "/dashboard": "dashboard", | |||
| "/jo": "Job Order", | |||
| "/jo/edit": "Edit Job Order", | |||
| }; | |||
| const Breadcrumb = () => { | |||
| @@ -0,0 +1,143 @@ | |||
| "use client" | |||
| import { SearchJoResult, SearchJoResultRequest, fetchJos } from "@/app/api/jo/actions"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { Criterion } from "../SearchBox"; | |||
| import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { decimalFormatter } from "@/app/utils/formatUtil"; | |||
| import { uniqBy, upperFirst } from "lodash"; | |||
| import SearchBox from "../SearchBox/SearchBox"; | |||
| interface Props { | |||
| defaultInputs: SearchJoResultRequest | |||
| } | |||
| type SearchQuery = Partial<Omit<SearchJoResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | |||
| const { t } = useTranslation("jo"); | |||
| const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]); | |||
| const [inputs, setInputs] = useState(defaultInputs); | |||
| const [pagingController, setPagingController] = useState( | |||
| defaultPagingController | |||
| ) | |||
| const [totalCount, setTotalCount] = useState(0) | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Name"), paramName: "name", type: "text" }, | |||
| ], [t]) | |||
| const columns = useMemo<Column<SearchJoResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: t("Details"), | |||
| onClick: (record) => onDetailClick(record), | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code") | |||
| }, | |||
| { | |||
| name: "name", | |||
| label: t("Name"), | |||
| }, | |||
| { | |||
| name: "reqQty", | |||
| label: t("Req. Qty"), | |||
| align: "right", | |||
| headerAlign: "right", | |||
| renderCell: (row) => { | |||
| return decimalFormatter.format(row.reqQty) | |||
| } | |||
| }, | |||
| { | |||
| name: "uom", | |||
| label: t("UoM"), | |||
| align: "left", | |||
| headerAlign: "left", | |||
| renderCell: (row) => { | |||
| return t(row.uom) | |||
| } | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (row) => { | |||
| return t(upperFirst(row.status)) | |||
| } | |||
| } | |||
| ], [] | |||
| ) | |||
| const refetchData = useCallback(async ( | |||
| query: Record<SearchParamNames, string> | SearchJoResultRequest, | |||
| actionType: "reset" | "search" | "paging", | |||
| ) => { | |||
| const params: SearchJoResultRequest = { | |||
| code: query.code, | |||
| name: query.name, | |||
| pageNum: pagingController.pageNum - 1, | |||
| pageSize: pagingController.pageSize, | |||
| } | |||
| const response = await fetchJos(params) | |||
| if (response) { | |||
| setTotalCount(response.total); | |||
| switch (actionType) { | |||
| case "reset": | |||
| case "search": | |||
| setFilteredJos(() => response.records); | |||
| break; | |||
| case "paging": | |||
| setFilteredJos((fs) => | |||
| uniqBy([...fs, ...response.records], "id"), | |||
| ); | |||
| break; | |||
| } | |||
| } | |||
| }, [pagingController, setPagingController]) | |||
| useEffect(() => { | |||
| refetchData(inputs, "paging"); | |||
| }, [pagingController]); | |||
| const onDetailClick = useCallback((record: SearchJoResult) => { | |||
| }, []) | |||
| const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | |||
| setInputs(() => ({ | |||
| code: query.code, | |||
| name: query.name | |||
| })) | |||
| refetchData(query, "search"); | |||
| }, []) | |||
| const onReset = useCallback(() => { | |||
| refetchData(defaultInputs, "paging"); | |||
| }, []) | |||
| return <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={onSearch} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<SearchJoResult> | |||
| items={filteredJos} | |||
| columns={columns} | |||
| setPagingController={setPagingController} | |||
| pagingController={pagingController} | |||
| totalCount={totalCount} | |||
| // isAutoPaging={false} | |||
| /> | |||
| </> | |||
| } | |||
| export default JoSearch; | |||
| @@ -0,0 +1,21 @@ | |||
| import React from "react"; | |||
| import GeneralLoading from "../General/GeneralLoading"; | |||
| import JoSearch from "./JoSearch"; | |||
| import { SearchJoResultRequest } from "@/app/api/jo/actions"; | |||
| interface SubComponents { | |||
| Loading: typeof GeneralLoading; | |||
| } | |||
| const JoSearchWrapper: React.FC & SubComponents = async () => { | |||
| const defaultInputs: SearchJoResultRequest = { | |||
| code: "", | |||
| name: "", | |||
| } | |||
| return <JoSearch defaultInputs={defaultInputs}/> | |||
| } | |||
| JoSearchWrapper.Loading = GeneralLoading; | |||
| export default JoSearchWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./JoSearchWrapper" | |||
| @@ -188,6 +188,18 @@ const NavigationContent: React.FC = () => { | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Job Order", | |||
| path: "", | |||
| children: [ | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Job Order", | |||
| path: "/jo", | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Settings", | |||
| @@ -245,7 +245,7 @@ const RSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||
| // setFilteredSchedules(items ?? []); | |||
| // setFilterObj({}); | |||
| // setTempSelectedValue({}); | |||
| refetchData(inputs, "reset"); | |||
| refetchData(defaultInputs, "reset"); | |||
| }, []); | |||
| return ( | |||