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