@@ -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>) => { | ||||