From da1fb872bf3b92e66cff3af4e1b8d1057c47effa Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Fri, 18 Jul 2025 17:57:21 +0800 Subject: [PATCH] [Job Order] JO can gen Pick Order now --- src/app/(main)/jo/edit/not-found.tsx | 19 ++++ src/app/(main)/jo/edit/page.tsx | 49 ++++++++ src/app/api/jo/actions.ts | 28 ++++- src/app/api/jo/index.ts | 35 ++++++ .../DetailedScheduleDetailWrapper.tsx | 2 - .../ViewByFGDetails.tsx | 2 +- src/components/JoSave/InfoCard.tsx | 84 ++++++++++++++ src/components/JoSave/JoSave.tsx | 107 ++++++++++++++++++ src/components/JoSave/JoSaveWrapper.tsx | 26 +++++ src/components/JoSave/PickTable.tsx | 91 +++++++++++++++ src/components/JoSave/index.ts | 1 + src/components/JoSearch/JoSearch.tsx | 8 +- 12 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 src/app/(main)/jo/edit/not-found.tsx create mode 100644 src/app/(main)/jo/edit/page.tsx create mode 100644 src/components/JoSave/InfoCard.tsx create mode 100644 src/components/JoSave/JoSave.tsx create mode 100644 src/components/JoSave/JoSaveWrapper.tsx create mode 100644 src/components/JoSave/PickTable.tsx create mode 100644 src/components/JoSave/index.ts diff --git a/src/app/(main)/jo/edit/not-found.tsx b/src/app/(main)/jo/edit/not-found.tsx new file mode 100644 index 0000000..6561158 --- /dev/null +++ b/src/app/(main)/jo/edit/not-found.tsx @@ -0,0 +1,19 @@ +import { getServerI18n } from "@/i18n"; +import { Stack, Typography, Link } from "@mui/material"; +import NextLink from "next/link"; + +export default async function NotFound() { + const { t } = await getServerI18n("schedule", "common"); + + return ( + + {t("Not Found")} + + {t("The job order page was not found!")} + + + {t("Return to all job orders")} + + + ); +} diff --git a/src/app/(main)/jo/edit/page.tsx b/src/app/(main)/jo/edit/page.tsx new file mode 100644 index 0000000..29b9c7f --- /dev/null +++ b/src/app/(main)/jo/edit/page.tsx @@ -0,0 +1,49 @@ +import { fetchJoDetail } from "@/app/api/jo"; +import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; +import JoSave from "@/components/JoSave"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Typography } from "@mui/material"; +import { isArray } from "lodash"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Edit Job Order Detail" +} + +type Props = SearchParams; + +const JoEdit: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("jo"); + const id = searchParams["id"]; + + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } + + try { + await fetchJoDetail(parseInt(id)) + } catch (e) { + if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { + console.log(e) + notFound(); + } + } + + + return ( + <> + + {t("Edit Job Order Detail")} + + + }> + + + + + ); +} + +export default JoEdit; \ No newline at end of file diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index d6bdb44..e9ea350 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -1,4 +1,6 @@ "use server"; + +import { cache } from 'react'; import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; import { Machine, Operator } from "."; import { BASE_API_URL } from "@/config/api"; @@ -15,7 +17,7 @@ export interface SearchJoResultResponse { total: number; } -export interface SearchJoResult{ +export interface SearchJoResult { id: number; code: string; name: string; @@ -24,6 +26,15 @@ export interface SearchJoResult{ status: string; } +export interface ReleaseJoRequest { + id: number; +} + +export interface ReleaseJoResponse { + id: number; + entity: { status: string } +} + export interface IsOperatorExistResponse { id: number | null; name: string; @@ -71,9 +82,9 @@ export const isCorrectMachineUsed = async (machineCode: string) => { }; -export const fetchJos = async (data?: SearchJoResultRequest) => { +export const fetchJos = cache(async (data?: SearchJoResultRequest) => { const queryStr = convertObjToURLSearchParams(data) - const response = await serverFetchJson( + const response = serverFetchJson( `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, { method: "GET", @@ -85,4 +96,13 @@ export const fetchJos = async (data?: SearchJoResultRequest) => { ) return response -} \ No newline at end of file +}) + +export const releaseJo = cache(async (data: ReleaseJoRequest) => { + return serverFetchJson(`${BASE_API_URL}/jo/release`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }) +}) \ No newline at end of file diff --git a/src/app/api/jo/index.ts b/src/app/api/jo/index.ts index f15ae8c..6b0af80 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -1,5 +1,9 @@ "server=only"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; + export interface Operator { id: number; name: string; @@ -12,3 +16,34 @@ export interface Machine { code: string; qrCode: string; } + +export interface JoDetail { + id: number; + code: string; + name: string; + reqQty: number; + outputQtyUom: string; + pickLines: JoDetailPickLine[]; + status: string; +} + +export interface JoDetailPickLine { + id: number; + code: string; + name: string; + lotNo: string; + reqQty: number; + uom: string; + status: string; +} + +export const fetchJoDetail = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/jo/detail/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json"}, + next: { + tags: ["jo"] + } + }) +}) \ No newline at end of file diff --git a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx index 862fcdf..349d90b 100644 --- a/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx +++ b/src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx @@ -1,5 +1,3 @@ -import { CreateItemInputs } from "@/app/api/settings/item/actions"; -import { fetchItem } from "@/app/api/settings/item"; import GeneralLoading from "@/components/General/GeneralLoading"; import DetailedScheduleDetailView from "@/components/DetailedScheduleDetail/DetailedScheduleDetailView"; import { ScheduleType, fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; diff --git a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx index 9e860dd..3902e39 100644 --- a/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/DetailedScheduleDetail/ViewByFGDetails.tsx @@ -163,7 +163,7 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit, type, onReleaseClick }, renderCell: (row) => { if (typeof row.demandQty == "number") { - return decimalFormatter.format(row.demandQty ?? 0); + return integerFormatter.format(row.demandQty ?? 0); } return row.demandQty; }, diff --git a/src/components/JoSave/InfoCard.tsx b/src/components/JoSave/InfoCard.tsx new file mode 100644 index 0000000..3a28009 --- /dev/null +++ b/src/components/JoSave/InfoCard.tsx @@ -0,0 +1,84 @@ +import { JoDetail } from "@/app/api/jo"; +import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; +import { Box, Card, CardContent, Grid, Stack, TextField } from "@mui/material"; +import { upperFirst } from "lodash"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +type Props = { + +}; + +const InfoCard: React.FC = ({ + +}) => { + const { t } = useTranslation(); + + const { control, getValues, register, watch } = useFormContext(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default InfoCard; \ No newline at end of file diff --git a/src/components/JoSave/JoSave.tsx b/src/components/JoSave/JoSave.tsx new file mode 100644 index 0000000..0bb820f --- /dev/null +++ b/src/components/JoSave/JoSave.tsx @@ -0,0 +1,107 @@ +"use client" +import { JoDetail } from "@/app/api/jo" +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; +import { useCallback, useState } from "react"; +import { Button, Stack, Typography } from "@mui/material"; +import StartIcon from "@mui/icons-material/Start"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { releaseJo } from "@/app/api/jo/actions"; +import InfoCard from "./InfoCard"; +import PickTable from "./PickTable"; + +type Props = { + id: number; + defaultValues: Partial | undefined; +} + +const JoSave: React.FC = ({ + defaultValues, + id, +}) => { + const { t } = useTranslation("jo") + const router = useRouter(); + const { setIsUploading } = useUploadContext(); + const [serverError, setServerError] = useState(""); + + const formProps = useForm({ + defaultValues: defaultValues + }) + + const handleBack = useCallback(() => { + router.replace(`/jo`) + }, []) + + const handleRelease = useCallback(async () => { + try { + setIsUploading(true) + if (id) { + console.log(id) + const response = await releaseJo({ id: id }) + console.log(response.entity.status) + if (response) { + formProps.setValue("status", response.entity.status) + console.log(formProps.watch("status")) + } + } + + } catch (e) { + // backend error + setServerError(t("An error has occurred. Please try again later.")); + console.log(e); + } finally { + setIsUploading(false) + } + }, []) + + const onSubmit = useCallback>(async (data, event) => { + console.log(data) + }, [t]) + + const onSubmitError = useCallback>((errors) => { + console.log(errors) + }, [t]) + + return <> + + + {serverError && ( + + {serverError} + + )} + { + formProps.watch("status").toLowerCase() === "planning" && ( + + + + )} + + + + + + + + +} + +export default JoSave; \ No newline at end of file diff --git a/src/components/JoSave/JoSaveWrapper.tsx b/src/components/JoSave/JoSaveWrapper.tsx new file mode 100644 index 0000000..edc7b4e --- /dev/null +++ b/src/components/JoSave/JoSaveWrapper.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import GeneralLoading from "../General/GeneralLoading"; +import { fetchJoDetail } from "@/app/api/jo"; +import JoSave from "./JoSave"; + +interface SubComponents { + Loading: typeof GeneralLoading; +} + +type JoSaveProps = { + id?: number; +} + +type Props = JoSaveProps + +const JoSaveWrapper: React.FC & SubComponents = async ({ + id, +}) => { + const jo = id ? await fetchJoDetail(id) : undefined + + return +} + +JoSaveWrapper.Loading = GeneralLoading; + +export default JoSaveWrapper; \ No newline at end of file diff --git a/src/components/JoSave/PickTable.tsx b/src/components/JoSave/PickTable.tsx new file mode 100644 index 0000000..e508181 --- /dev/null +++ b/src/components/JoSave/PickTable.tsx @@ -0,0 +1,91 @@ +import { JoDetail } from "@/app/api/jo"; +import { decimalFormatter } from "@/app/utils/formatUtil"; +import { GridColDef } from "@mui/x-data-grid"; +import { isEmpty, upperFirst } from "lodash"; +import { useMemo } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; + +type Props = { + +}; + +const PickTable: React.FC = ({ + +}) => { + const { t } = useTranslation("jo") + const { + watch + } = useFormContext() + + const columns = useMemo(() => [ + { + field: "code", + headerName: t("Code"), + flex: 1, + }, + { + field: "name", + headerName: t("Name"), + flex: 1, + }, + { + field: "lotNo", + headerName: t("Lot No."), + flex: 1, + renderCell: (row) => { + return isEmpty(row.value) ? "N/A" : row.value + }, + }, + { + field: "reqQty", + headerName: t("Req. Qty"), + flex: 1, + align: "right", + headerAlign: "right", + renderCell: (row) => { + return decimalFormatter.format(row.value) + }, + }, + { + field: "uom", + headerName: t("UoM"), + flex: 1, + align: "left", + headerAlign: "left", + }, + + { + field: "status", + headerName: t("Status"), + flex: 1, + renderCell: (row) => { + return upperFirst(row.value) + }, + }, + ], []) + + return ( + <> + + + ) +} + +export default PickTable; \ No newline at end of file diff --git a/src/components/JoSave/index.ts b/src/components/JoSave/index.ts new file mode 100644 index 0000000..4c1e410 --- /dev/null +++ b/src/components/JoSave/index.ts @@ -0,0 +1 @@ +export { default } from "./JoSaveWrapper"; \ No newline at end of file diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 727f16a..7fb407f 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -5,9 +5,10 @@ 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 { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; import { uniqBy, upperFirst } from "lodash"; import SearchBox from "../SearchBox/SearchBox"; +import { useRouter } from "next/navigation"; interface Props { defaultInputs: SearchJoResultRequest @@ -19,6 +20,7 @@ type SearchParamNames = keyof SearchQuery; const JoSearch: React.FC = ({ defaultInputs }) => { const { t } = useTranslation("jo"); + const router = useRouter() const [filteredJos, setFilteredJos] = useState([]); const [inputs, setInputs] = useState(defaultInputs); const [pagingController, setPagingController] = useState( @@ -53,7 +55,7 @@ const JoSearch: React.FC = ({ defaultInputs }) => { align: "right", headerAlign: "right", renderCell: (row) => { - return decimalFormatter.format(row.reqQty) + return integerFormatter.format(row.reqQty) } }, { @@ -108,7 +110,7 @@ const JoSearch: React.FC = ({ defaultInputs }) => { }, [pagingController]); const onDetailClick = useCallback((record: SearchJoResult) => { - + router.push(`/jo/edit?id=${record.id}`) }, []) const onSearch = useCallback((query: Record) => {