| @@ -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 ( | |||||
| <Stack spacing={2}> | |||||
| <Typography variant="h4">{t("Not Found")}</Typography> | |||||
| <Typography variant="body1"> | |||||
| {t("The job order page was not found!")} | |||||
| </Typography> | |||||
| <Link href="/settings/scheduling" component={NextLink} variant="body2"> | |||||
| {t("Return to all job orders")} | |||||
| </Link> | |||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| @@ -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<Props> = 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 ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Edit Job Order Detail")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["jo", "common"]}> | |||||
| <Suspense fallback={<JoSave.Loading />}> | |||||
| <JoSave id={parseInt(id)} /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default JoEdit; | |||||
| @@ -1,4 +1,6 @@ | |||||
| "use server"; | "use server"; | ||||
| import { cache } from 'react'; | |||||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { Machine, Operator } from "."; | import { Machine, Operator } from "."; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| @@ -15,7 +17,7 @@ export interface SearchJoResultResponse { | |||||
| total: number; | total: number; | ||||
| } | } | ||||
| export interface SearchJoResult{ | |||||
| export interface SearchJoResult { | |||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| name: string; | name: string; | ||||
| @@ -24,6 +26,15 @@ export interface SearchJoResult{ | |||||
| status: string; | status: string; | ||||
| } | } | ||||
| export interface ReleaseJoRequest { | |||||
| id: number; | |||||
| } | |||||
| export interface ReleaseJoResponse { | |||||
| id: number; | |||||
| entity: { status: string } | |||||
| } | |||||
| export interface IsOperatorExistResponse<T> { | export interface IsOperatorExistResponse<T> { | ||||
| id: number | null; | id: number | null; | ||||
| name: string; | 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 queryStr = convertObjToURLSearchParams(data) | ||||
| const response = await serverFetchJson<SearchJoResultResponse>( | |||||
| const response = serverFetchJson<SearchJoResultResponse>( | |||||
| `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | ||||
| { | { | ||||
| method: "GET", | method: "GET", | ||||
| @@ -85,4 +96,13 @@ export const fetchJos = async (data?: SearchJoResultRequest) => { | |||||
| ) | ) | ||||
| return response | return response | ||||
| } | |||||
| }) | |||||
| export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||||
| return serverFetchJson<ReleaseJoResponse>(`${BASE_API_URL}/jo/release`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }) | |||||
| }) | |||||
| @@ -1,5 +1,9 @@ | |||||
| "server=only"; | "server=only"; | ||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { cache } from "react"; | |||||
| export interface Operator { | export interface Operator { | ||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| @@ -12,3 +16,34 @@ export interface Machine { | |||||
| code: string; | code: string; | ||||
| qrCode: 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<JoDetail>(`${BASE_API_URL}/jo/detail/${id}`, | |||||
| { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json"}, | |||||
| next: { | |||||
| tags: ["jo"] | |||||
| } | |||||
| }) | |||||
| }) | |||||
| @@ -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 GeneralLoading from "@/components/General/GeneralLoading"; | ||||
| import DetailedScheduleDetailView from "@/components/DetailedScheduleDetail/DetailedScheduleDetailView"; | import DetailedScheduleDetailView from "@/components/DetailedScheduleDetail/DetailedScheduleDetailView"; | ||||
| import { ScheduleType, fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; | import { ScheduleType, fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; | ||||
| @@ -163,7 +163,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||||
| }, | }, | ||||
| renderCell: (row) => { | renderCell: (row) => { | ||||
| if (typeof row.demandQty == "number") { | if (typeof row.demandQty == "number") { | ||||
| return decimalFormatter.format(row.demandQty ?? 0); | |||||
| return integerFormatter.format(row.demandQty ?? 0); | |||||
| } | } | ||||
| return row.demandQty; | return row.demandQty; | ||||
| }, | }, | ||||
| @@ -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<Props> = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { control, getValues, register, watch } = useFormContext<JoDetail>(); | |||||
| return ( | |||||
| <Card sx={{ display: "block" }}> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| // { | |||||
| // ...register("status") | |||||
| // } | |||||
| label={t("Status")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| value={`${t(upperFirst(watch("status")))}`} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}/> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| { | |||||
| ...register("code") | |||||
| } | |||||
| label={t("Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| { | |||||
| ...register("name") | |||||
| } | |||||
| label={t("Name")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| // { | |||||
| // ...register("name") | |||||
| // } | |||||
| label={t("Req. Qty")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| defaultValue={`${integerFormatter.format(watch("reqQty"))}`} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| { | |||||
| ...register("outputQtyUom") | |||||
| } | |||||
| label={t("UoM")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ) | |||||
| } | |||||
| export default InfoCard; | |||||
| @@ -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<JoDetail> | undefined; | |||||
| } | |||||
| const JoSave: React.FC<Props> = ({ | |||||
| defaultValues, | |||||
| id, | |||||
| }) => { | |||||
| const { t } = useTranslation("jo") | |||||
| const router = useRouter(); | |||||
| const { setIsUploading } = useUploadContext(); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const formProps = useForm<JoDetail>({ | |||||
| 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<SubmitHandler<JoDetail>>(async (data, event) => { | |||||
| console.log(data) | |||||
| }, [t]) | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<JoDetail>>((errors) => { | |||||
| console.log(errors) | |||||
| }, [t]) | |||||
| return <> | |||||
| <FormProvider {...formProps}> | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| {serverError && ( | |||||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||||
| {serverError} | |||||
| </Typography> | |||||
| )} | |||||
| { | |||||
| formProps.watch("status").toLowerCase() === "planning" && ( | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<StartIcon />} | |||||
| onClick={handleRelease} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| <InfoCard /> | |||||
| <PickTable /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<ArrowBackIcon />} | |||||
| onClick={handleBack} | |||||
| > | |||||
| {t("Back")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| </> | |||||
| } | |||||
| export default JoSave; | |||||
| @@ -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<Props> & SubComponents = async ({ | |||||
| id, | |||||
| }) => { | |||||
| const jo = id ? await fetchJoDetail(id) : undefined | |||||
| return <JoSave id={id} defaultValues={jo}/> | |||||
| } | |||||
| JoSaveWrapper.Loading = GeneralLoading; | |||||
| export default JoSaveWrapper; | |||||
| @@ -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<Props> = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation("jo") | |||||
| const { | |||||
| watch | |||||
| } = useFormContext<JoDetail>() | |||||
| const columns = useMemo<GridColDef[]>(() => [ | |||||
| { | |||||
| 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 ( | |||||
| <> | |||||
| <StyledDataGrid | |||||
| sx={{ | |||||
| "--DataGrid-overlayHeight": "100px", | |||||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||||
| border: "1px solid", | |||||
| borderColor: "error.main", | |||||
| }, | |||||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||||
| border: "1px solid", | |||||
| borderColor: "warning.main", | |||||
| }, | |||||
| }} | |||||
| disableColumnMenu | |||||
| rows={watch("pickLines")} | |||||
| columns={columns} | |||||
| /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default PickTable; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./JoSaveWrapper"; | |||||
| @@ -5,9 +5,10 @@ import { useTranslation } from "react-i18next"; | |||||
| import { Criterion } from "../SearchBox"; | import { Criterion } from "../SearchBox"; | ||||
| import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; | import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; | ||||
| import { EditNote } from "@mui/icons-material"; | 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 { uniqBy, upperFirst } from "lodash"; | ||||
| import SearchBox from "../SearchBox/SearchBox"; | import SearchBox from "../SearchBox/SearchBox"; | ||||
| import { useRouter } from "next/navigation"; | |||||
| interface Props { | interface Props { | ||||
| defaultInputs: SearchJoResultRequest | defaultInputs: SearchJoResultRequest | ||||
| @@ -19,6 +20,7 @@ type SearchParamNames = keyof SearchQuery; | |||||
| const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | ||||
| const { t } = useTranslation("jo"); | const { t } = useTranslation("jo"); | ||||
| const router = useRouter() | |||||
| const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]); | const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]); | ||||
| const [inputs, setInputs] = useState(defaultInputs); | const [inputs, setInputs] = useState(defaultInputs); | ||||
| const [pagingController, setPagingController] = useState( | const [pagingController, setPagingController] = useState( | ||||
| @@ -53,7 +55,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (row) => { | renderCell: (row) => { | ||||
| return decimalFormatter.format(row.reqQty) | |||||
| return integerFormatter.format(row.reqQty) | |||||
| } | } | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -108,7 +110,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => { | |||||
| }, [pagingController]); | }, [pagingController]); | ||||
| const onDetailClick = useCallback((record: SearchJoResult) => { | const onDetailClick = useCallback((record: SearchJoResult) => { | ||||
| router.push(`/jo/edit?id=${record.id}`) | |||||
| }, []) | }, []) | ||||
| const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | ||||