| @@ -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"; | |||
| 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<T> { | |||
| 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<SearchJoResultResponse>( | |||
| const response = serverFetchJson<SearchJoResultResponse>( | |||
| `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`, | |||
| { | |||
| method: "GET", | |||
| @@ -85,4 +96,13 @@ export const fetchJos = async (data?: SearchJoResultRequest) => { | |||
| ) | |||
| 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"; | |||
| 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<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 DetailedScheduleDetailView from "@/components/DetailedScheduleDetail/DetailedScheduleDetailView"; | |||
| import { ScheduleType, fetchDetailedProdScheduleDetail } from "@/app/api/scheduling"; | |||
| @@ -163,7 +163,7 @@ const ViewByFGDetails: React.FC<Props> = ({ 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; | |||
| }, | |||
| @@ -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 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<Props> = ({ defaultInputs }) => { | |||
| const { t } = useTranslation("jo"); | |||
| const router = useRouter() | |||
| const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]); | |||
| const [inputs, setInputs] = useState(defaultInputs); | |||
| const [pagingController, setPagingController] = useState( | |||
| @@ -53,7 +55,7 @@ const JoSearch: React.FC<Props> = ({ 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<Props> = ({ defaultInputs }) => { | |||
| }, [pagingController]); | |||
| const onDetailClick = useCallback((record: SearchJoResult) => { | |||
| router.push(`/jo/edit?id=${record.id}`) | |||
| }, []) | |||
| const onSearch = useCallback((query: Record<SearchParamNames, string>) => { | |||