From d3417c5c1eeff449d41ada4627a2328862dafb47 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 30 Jul 2025 17:49:05 +0800 Subject: [PATCH] [Job Order] Can manual create job order --- src/app/(main)/jo/page.tsx | 3 + src/app/api/bom/index.ts | 21 ++ src/app/api/jo/actions.ts | 21 ++ src/app/api/jo/index.ts | 5 +- src/app/utils/formatUtil.ts | 8 + src/components/JoSave/InfoCard.tsx | 2 +- src/components/JoSearch/JoCreateFormModal.tsx | 243 ++++++++++++++++++ src/components/JoSearch/JoSearch.tsx | 53 +++- src/components/JoSearch/JoSearchWrapper.tsx | 9 +- 9 files changed, 356 insertions(+), 9 deletions(-) create mode 100644 src/app/api/bom/index.ts create mode 100644 src/components/JoSearch/JoCreateFormModal.tsx diff --git a/src/app/(main)/jo/page.tsx b/src/app/(main)/jo/page.tsx index 8922424..1020c94 100644 --- a/src/app/(main)/jo/page.tsx +++ b/src/app/(main)/jo/page.tsx @@ -1,3 +1,4 @@ +import { preloadBomCombo } from "@/app/api/bom"; import JoSearch from "@/components/JoSearch"; import { I18nProvider, getServerI18n } from "@/i18n"; import { Stack, Typography } from "@mui/material"; @@ -11,6 +12,8 @@ export const metadata: Metadata = { const jo: React.FC = async () => { const { t } = await getServerI18n("jo"); + preloadBomCombo() + return ( <> { + fetchBomCombo() +}) + +export const fetchBomCombo = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/bom/combo`, { + next: { tags: ["bomCombo"] }, + }) +}) + + diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index e9ea350..9521bac 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -7,6 +7,18 @@ import { BASE_API_URL } from "@/config/api"; import { revalidateTag } from "next/cache"; import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; +export interface SaveJo { + bomId: number; + planStart: string; + planEnd: string; + reqQty: number; + type: string; +} + +export interface SaveJoResponse { + id: number; +} + export interface SearchJoResultRequest extends Pageable { code: string; name: string; @@ -105,4 +117,13 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => { body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }) +}) + +export const manualCreateJo = cache(async (data: SaveJo) => { + console.log(data) + return serverFetchJson(`${BASE_API_URL}/jo/manualCreate`, { + 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 6b0af80..85bd195 100644 --- a/src/app/api/jo/index.ts +++ b/src/app/api/jo/index.ts @@ -22,9 +22,12 @@ export interface JoDetail { code: string; name: string; reqQty: number; - outputQtyUom: string; + uom: string; pickLines: JoDetailPickLine[]; status: string; + planStart: number[]; + planEnd: number[]; + type: string; } export interface JoDetailPickLine { diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 421a473..31142c9 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -68,6 +68,14 @@ export const dayjsToDateString = (date: Dayjs) => { return date.format(OUTPUT_DATE_FORMAT); }; +export const dayjsToInputDateString = (date: Dayjs) => { + return date.format(INPUT_DATE_FORMAT); +}; + +export const dayjsToInputDateTimeString = (date: Dayjs) => { + return date.format(`${INPUT_DATE_FORMAT}T${OUTPUT_TIME_FORMAT}`); +}; + export const stockInLineStatusMap: { [status: string]: number } = { draft: 0, pending: 1, diff --git a/src/components/JoSave/InfoCard.tsx b/src/components/JoSave/InfoCard.tsx index 3a28009..2e71216 100644 --- a/src/components/JoSave/InfoCard.tsx +++ b/src/components/JoSave/InfoCard.tsx @@ -67,7 +67,7 @@ const InfoCard: React.FC = ({ void; + onSearch: () => void; +} + +const JoCreateFormModal: React.FC = ({ + open, + bomCombo, + onClose, + onSearch, +}) => { + const { t } = useTranslation("jo"); + const formProps = useForm({ + mode: "onChange", + }); + const { reset, trigger, watch, control, register, formState: { errors } } = formProps + + const onModalClose = useCallback(() => { + reset() + onClose() + }, []) + + const handleAutoCompleteChange = useCallback((event: SyntheticEvent, value: BomCombo, onChange: (...event: any[]) => void) => { + onChange(value.id) + }, []) + + const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { + if (value != null) { + const updatedValue = dayjsToInputDateTimeString(value) + onChange(updatedValue) + } else { + onChange(value) + } + }, []) + + const onSubmit = useCallback>(async (data) => { + data.type = "manual" + const response = await manualCreateJo(data) + if (response) { + onSearch(); + onModalClose(); + } + }, []) + + const onSubmitError = useCallback>((error) => { + console.log(error) + }, []) + + const planStart = watch("planStart") + const planEnd = watch("planEnd") + useEffect(() => { + trigger(['planStart', 'planEnd']); + }, [trigger, planStart, planEnd]) + + return ( + + + + + + + + + {t("Create Job Order")} + + + isFinite(value) + }} + render={({ field, fieldState: { error } }) => ( + { + handleAutoCompleteChange(event, value, field.onChange) + }} + onBlur={field.onBlur} + renderInput={(params) => ( + + )} + /> + )} + /> + + + value > 0 + })} + label={t("Req. Qty")} + fullWidth + error={Boolean(errors.reqQty)} + variant="outlined" + type="number" + /> + + {/* + dateStringToDayjs(value).isValid(), + isBeforePlanEnd: (value) => { + const planStartDayjs = dateStringToDayjs(value) + const planEndDayjs = dateStringToDayjs(planEnd) + return planStartDayjs.isBefore(planEndDayjs) || planStartDayjs.isSame(planEndDayjs) + } + } + }} + render={({ field, fieldState: { error } }) => ( + { + handleDateTimePickerChange(newValue, field.onChange) + }} + slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }} + /> + )} + /> + + + dateStringToDayjs(value).isValid(), + isBeforePlanEnd: (value) => { + const planStartDayjs = dateStringToDayjs(planStart) + const planEndDayjs = dateStringToDayjs(value) + return planEndDayjs.isAfter(planStartDayjs) || planEndDayjs.isSame(planStartDayjs) + } + } + }} + render={({ field, fieldState: { error } }) => ( + { + handleDateTimePickerChange(newValue, field.onChange) + }} + slotProps={{ textField: { fullWidth: true } }} + /> + )} + /> + */} + + + + + + + + + + + ) +} + +export default JoCreateFormModal; \ No newline at end of file diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 7fb407f..89d95b4 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -6,19 +6,26 @@ import { Criterion } from "../SearchBox"; import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; import { EditNote } from "@mui/icons-material"; import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; -import { uniqBy, upperFirst } from "lodash"; +import { orderBy, uniqBy, upperFirst } from "lodash"; import SearchBox from "../SearchBox/SearchBox"; import { useRouter } from "next/navigation"; +import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; +import { JoDetail } from "@/app/api/jo"; +import { Button, Stack } from "@mui/material"; +import { BomCombo } from "@/app/api/bom"; +import JoCreateFormModal from "./JoCreateFormModal"; +import AddIcon from '@mui/icons-material/Add'; interface Props { - defaultInputs: SearchJoResultRequest + defaultInputs: SearchJoResultRequest, + bomCombo: BomCombo[] } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const JoSearch: React.FC = ({ defaultInputs }) => { +const JoSearch: React.FC = ({ defaultInputs, bomCombo }) => { const { t } = useTranslation("jo"); const router = useRouter() const [filteredJos, setFilteredJos] = useState([]); @@ -27,6 +34,7 @@ const JoSearch: React.FC = ({ defaultInputs }) => { defaultPagingController ) const [totalCount, setTotalCount] = useState(0) + const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) const searchCriteria: Criterion[] = useMemo(() => [ { label: t("Code"), paramName: "code", type: "text" }, @@ -94,19 +102,22 @@ const JoSearch: React.FC = ({ defaultInputs }) => { switch (actionType) { case "reset": case "search": - setFilteredJos(() => response.records); + setFilteredJos(() => orderBy(response.records, ["id"], ["desc"])); break; case "paging": setFilteredJos((fs) => - uniqBy([...fs, ...response.records], "id"), + orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]), ); break; } } }, [pagingController, setPagingController]) - useEffect(() => { + const searchDataByPage = useCallback(() => { refetchData(inputs, "paging"); + }, [inputs]) + useEffect(() => { + searchDataByPage(); }, [pagingController]); const onDetailClick = useCallback((record: SearchJoResult) => { @@ -125,7 +136,31 @@ const JoSearch: React.FC = ({ defaultInputs }) => { refetchData(defaultInputs, "paging"); }, []) + // Manual Create Jo Related + + const onOpenCreateJoModal = useCallback(() => { + setIsCreateJoModalOpen(() => true) + }, []) + + const onCloseCreateJoModal = useCallback(() => { + setIsCreateJoModalOpen(() => false) + }, []) + return <> + + + = ({ defaultInputs }) => { totalCount={totalCount} // isAutoPaging={false} /> + } diff --git a/src/components/JoSearch/JoSearchWrapper.tsx b/src/components/JoSearch/JoSearchWrapper.tsx index 391f476..b44345c 100644 --- a/src/components/JoSearch/JoSearchWrapper.tsx +++ b/src/components/JoSearch/JoSearchWrapper.tsx @@ -2,6 +2,7 @@ import React from "react"; import GeneralLoading from "../General/GeneralLoading"; import JoSearch from "./JoSearch"; import { SearchJoResultRequest } from "@/app/api/jo/actions"; +import { fetchBomCombo } from "@/app/api/bom"; interface SubComponents { Loading: typeof GeneralLoading; @@ -12,8 +13,14 @@ const JoSearchWrapper: React.FC & SubComponents = async () => { code: "", name: "", } + + const [ + bomCombo + ] = await Promise.all([ + fetchBomCombo() + ]) - return + return } JoSearchWrapper.Loading = GeneralLoading;