Przeglądaj źródła

[Job Order] JO can gen Pick Order now

master
cyril.tsui 1 miesiąc temu
rodzic
commit
da1fb872bf
12 zmienionych plików z 442 dodań i 10 usunięć
  1. +19
    -0
      src/app/(main)/jo/edit/not-found.tsx
  2. +49
    -0
      src/app/(main)/jo/edit/page.tsx
  3. +24
    -4
      src/app/api/jo/actions.ts
  4. +35
    -0
      src/app/api/jo/index.ts
  5. +0
    -2
      src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx
  6. +1
    -1
      src/components/DetailedScheduleDetail/ViewByFGDetails.tsx
  7. +84
    -0
      src/components/JoSave/InfoCard.tsx
  8. +107
    -0
      src/components/JoSave/JoSave.tsx
  9. +26
    -0
      src/components/JoSave/JoSaveWrapper.tsx
  10. +91
    -0
      src/components/JoSave/PickTable.tsx
  11. +1
    -0
      src/components/JoSave/index.ts
  12. +5
    -3
      src/components/JoSearch/JoSearch.tsx

+ 19
- 0
src/app/(main)/jo/edit/not-found.tsx Wyświetl plik

@@ -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>
);
}

+ 49
- 0
src/app/(main)/jo/edit/page.tsx Wyświetl plik

@@ -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;

+ 24
- 4
src/app/api/jo/actions.ts Wyświetl plik

@@ -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" },
})
})

+ 35
- 0
src/app/api/jo/index.ts Wyświetl plik

@@ -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"]
}
})
})

+ 0
- 2
src/components/DetailedScheduleDetail/DetailedScheduleDetailWrapper.tsx Wyświetl plik

@@ -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";


+ 1
- 1
src/components/DetailedScheduleDetail/ViewByFGDetails.tsx Wyświetl plik

@@ -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;
},


+ 84
- 0
src/components/JoSave/InfoCard.tsx Wyświetl plik

@@ -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;

+ 107
- 0
src/components/JoSave/JoSave.tsx Wyświetl plik

@@ -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;

+ 26
- 0
src/components/JoSave/JoSaveWrapper.tsx Wyświetl plik

@@ -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;

+ 91
- 0
src/components/JoSave/PickTable.tsx Wyświetl plik

@@ -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;

+ 1
- 0
src/components/JoSave/index.ts Wyświetl plik

@@ -0,0 +1 @@
export { default } from "./JoSaveWrapper";

+ 5
- 3
src/components/JoSearch/JoSearch.tsx Wyświetl plik

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


Ładowanie…
Anuluj
Zapisz