CANCERYS\kw093 3 semanas atrás
pai
commit
837911c055
9 arquivos alterados com 356 adições e 9 exclusões
  1. +3
    -0
      src/app/(main)/jo/page.tsx
  2. +21
    -0
      src/app/api/bom/index.ts
  3. +21
    -0
      src/app/api/jo/actions.ts
  4. +4
    -1
      src/app/api/jo/index.ts
  5. +8
    -0
      src/app/utils/formatUtil.ts
  6. +1
    -1
      src/components/JoSave/InfoCard.tsx
  7. +243
    -0
      src/components/JoSearch/JoCreateFormModal.tsx
  8. +47
    -6
      src/components/JoSearch/JoSearch.tsx
  9. +8
    -1
      src/components/JoSearch/JoSearchWrapper.tsx

+ 3
- 0
src/app/(main)/jo/page.tsx Ver arquivo

@@ -1,3 +1,4 @@
import { preloadBomCombo } from "@/app/api/bom";
import JoSearch from "@/components/JoSearch";
import { I18nProvider, getServerI18n } from "@/i18n";
import { Stack, Typography } from "@mui/material";
@@ -11,6 +12,8 @@ export const metadata: Metadata = {
const jo: React.FC = async () => {
const { t } = await getServerI18n("jo");

preloadBomCombo()

return (
<>
<Stack


+ 21
- 0
src/app/api/bom/index.ts Ver arquivo

@@ -0,0 +1,21 @@
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";

export interface BomCombo {
id: number;
value: number;
label: string;
}

export const preloadBomCombo = (() => {
fetchBomCombo()
})

export const fetchBomCombo = cache(async () => {
return serverFetchJson<BomCombo[]>(`${BASE_API_URL}/bom/combo`, {
next: { tags: ["bomCombo"] },
})
})



+ 21
- 0
src/app/api/jo/actions.ts Ver arquivo

@@ -7,6 +7,18 @@ import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil";

export interface SaveJo {
bomId: number;
planStart: string;
planEnd: string;
reqQty: number;
type: string;
}

export interface SaveJoResponse {
id: number;
}

export interface SearchJoResultRequest extends Pageable {
code: string;
name: string;
@@ -105,4 +117,13 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
})
})

export const manualCreateJo = cache(async (data: SaveJo) => {
console.log(data)
return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" }
})
})

+ 4
- 1
src/app/api/jo/index.ts Ver arquivo

@@ -22,9 +22,12 @@ export interface JoDetail {
code: string;
name: string;
reqQty: number;
outputQtyUom: string;
uom: string;
pickLines: JoDetailPickLine[];
status: string;
planStart: number[];
planEnd: number[];
type: string;
}

export interface JoDetailPickLine {


+ 8
- 0
src/app/utils/formatUtil.ts Ver arquivo

@@ -68,6 +68,14 @@ export const dayjsToDateString = (date: Dayjs) => {
return date.format(OUTPUT_DATE_FORMAT);
};

export const dayjsToInputDateString = (date: Dayjs) => {
return date.format(INPUT_DATE_FORMAT);
};

export const dayjsToInputDateTimeString = (date: Dayjs) => {
return date.format(`${INPUT_DATE_FORMAT}T${OUTPUT_TIME_FORMAT}`);
};

export const stockInLineStatusMap: { [status: string]: number } = {
draft: 0,
pending: 1,


+ 1
- 1
src/components/JoSave/InfoCard.tsx Ver arquivo

@@ -67,7 +67,7 @@ const InfoCard: React.FC<Props> = ({
<Grid item xs={6}>
<TextField
{
...register("outputQtyUom")
...register("uom")
}
label={t("UoM")}
fullWidth


+ 243
- 0
src/components/JoSearch/JoCreateFormModal.tsx Ver arquivo

@@ -0,0 +1,243 @@
import { BomCombo } from "@/app/api/bom";
import { JoDetail } from "@/app/api/jo";
import { SaveJo, manualCreateJo } from "@/app/api/jo/actions";
import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToInputDateString, dayjsToInputDateTimeString } from "@/app/utils/formatUtil";
import { Check } from "@mui/icons-material";
import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography } from "@mui/material";
import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { isFinite } from "lodash";
import React, { SetStateAction, SyntheticEvent, useCallback, useEffect } from "react";
import { Controller, FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

interface Props {
open: boolean;
bomCombo: BomCombo[];
onClose: () => void;
onSearch: () => void;
}

const JoCreateFormModal: React.FC<Props> = ({
open,
bomCombo,
onClose,
onSearch,
}) => {
const { t } = useTranslation("jo");
const formProps = useForm<SaveJo>({
mode: "onChange",
});
const { reset, trigger, watch, control, register, formState: { errors } } = formProps

const onModalClose = useCallback(() => {
reset()
onClose()
}, [])

const handleAutoCompleteChange = useCallback((event: SyntheticEvent<Element, Event>, value: BomCombo, onChange: (...event: any[]) => void) => {
onChange(value.id)
}, [])

const handleDateTimePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => {
if (value != null) {
const updatedValue = dayjsToInputDateTimeString(value)
onChange(updatedValue)
} else {
onChange(value)
}
}, [])

const onSubmit = useCallback<SubmitHandler<SaveJo>>(async (data) => {
data.type = "manual"
const response = await manualCreateJo(data)
if (response) {
onSearch();
onModalClose();
}
}, [])

const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => {
console.log(error)
}, [])

const planStart = watch("planStart")
const planEnd = watch("planEnd")
useEffect(() => {
trigger(['planStart', 'planEnd']);
}, [trigger, planStart, planEnd])

return (
<Modal
open={open}
onClose={onModalClose}
>
<Card
style={{
flex: 10,
marginBottom: "20px",
width: "90%",
// height: "80%",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
}}
>
<Box
sx={{
display: "flex",
"flex-direction": "column",
padding: "20px",
height: "100%", //'30rem',
width: "100%",
"& .actions": {
color: "text.secondary",
},
"& .header": {
// border: 1,
// 'border-width': '1px',
// 'border-color': 'grey',
},
"& .textPrimary": {
color: "text.primary",
},
}}
>
<FormProvider {...formProps}>
<Stack
// spacing={2}
component="form"
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}
>
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} md={12}>
<Typography variant="h6">{t("Create Job Order")}</Typography>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}
name="bomId"
rules={{
required: "Bom required!",
validate: (value) => isFinite(value)
}}
render={({ field, fieldState: { error } }) => (
<Autocomplete
disableClearable
options={bomCombo}
onChange={(event, value) => {
handleAutoCompleteChange(event, value, field.onChange)
}}
onBlur={field.onBlur}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(error)}
variant="outlined"
label={t("Bom")}
/>
)}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<TextField
{...register("reqQty", {
required: "Req. Qty. required!",
validate: (value) => value > 0
})}
label={t("Req. Qty")}
fullWidth
error={Boolean(errors.reqQty)}
variant="outlined"
type="number"
/>
</Grid>
{/* <Grid item xs={12} sm={12} md={6}>
<Controller
control={control}
name="planStart"
rules={{
required: "Plan start required!",
validate: {
isValid: (value) => dateStringToDayjs(value).isValid(),
isBeforePlanEnd: (value) => {
const planStartDayjs = dateStringToDayjs(value)
const planEndDayjs = dateStringToDayjs(planEnd)
return planStartDayjs.isBefore(planEndDayjs) || planStartDayjs.isSame(planEndDayjs)
}
}
}}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Plan Start")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) => {
handleDateTimePickerChange(newValue, field.onChange)
}}
slotProps={{ textField: { fullWidth: true, error: Boolean(error) } }}
/>
)}
/>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<Controller
control={control}
name="planEnd"
rules={{
required: "Plan end required!",
validate: {
isValid: (value) => dateStringToDayjs(value).isValid(),
isBeforePlanEnd: (value) => {
const planStartDayjs = dateStringToDayjs(planStart)
const planEndDayjs = dateStringToDayjs(value)
return planEndDayjs.isAfter(planStartDayjs) || planEndDayjs.isSame(planStartDayjs)
}
}
}}
render={({ field, fieldState: { error } }) => (
<DateTimePicker
label={t("Plan End")}
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
onChange={(newValue: Dayjs | null) => {
handleDateTimePickerChange(newValue, field.onChange)
}}
slotProps={{ textField: { fullWidth: true } }}
/>
)}
/>
</Grid> */}
</Grid>
<Stack
direction="row"
justifyContent="flex-end"
spacing={2}
sx={{ mt: 2 }}
>
<Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
>
{t("Create")}
</Button>
</Stack>
</LocalizationProvider>
</Stack>
</FormProvider>
</Box>
</Card>
</Modal>
)
}

export default JoCreateFormModal;

+ 47
- 6
src/components/JoSearch/JoSearch.tsx Ver arquivo

@@ -6,19 +6,26 @@ import { Criterion } from "../SearchBox";
import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults";
import { EditNote } from "@mui/icons-material";
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil";
import { uniqBy, upperFirst } from "lodash";
import { orderBy, uniqBy, upperFirst } from "lodash";
import SearchBox from "../SearchBox/SearchBox";
import { useRouter } from "next/navigation";
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
import { JoDetail } from "@/app/api/jo";
import { Button, Stack } from "@mui/material";
import { BomCombo } from "@/app/api/bom";
import JoCreateFormModal from "./JoCreateFormModal";
import AddIcon from '@mui/icons-material/Add';

interface Props {
defaultInputs: SearchJoResultRequest
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
}

type SearchQuery = Partial<Omit<SearchJoResult, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs }) => {
const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
const { t } = useTranslation("jo");
const router = useRouter()
const [filteredJos, setFilteredJos] = useState<SearchJoResult[]>([]);
@@ -27,6 +34,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => {
defaultPagingController
)
const [totalCount, setTotalCount] = useState(0)
const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
{ label: t("Code"), paramName: "code", type: "text" },
@@ -94,19 +102,22 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => {
switch (actionType) {
case "reset":
case "search":
setFilteredJos(() => response.records);
setFilteredJos(() => orderBy(response.records, ["id"], ["desc"]));
break;
case "paging":
setFilteredJos((fs) =>
uniqBy([...fs, ...response.records], "id"),
orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]),
);
break;
}
}
}, [pagingController, setPagingController])

useEffect(() => {
const searchDataByPage = useCallback(() => {
refetchData(inputs, "paging");
}, [inputs])
useEffect(() => {
searchDataByPage();
}, [pagingController]);

const onDetailClick = useCallback((record: SearchJoResult) => {
@@ -125,7 +136,31 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => {
refetchData(defaultInputs, "paging");
}, [])

// Manual Create Jo Related

const onOpenCreateJoModal = useCallback(() => {
setIsCreateJoModalOpen(() => true)
}, [])

const onCloseCreateJoModal = useCallback(() => {
setIsCreateJoModalOpen(() => false)
}, [])

return <>
<Stack
direction="row"
justifyContent="flex-end"
spacing={2}
sx={{ mt: 2 }}
>
<Button
variant="outlined"
startIcon={<AddIcon />}
onClick={onOpenCreateJoModal}
>
{t("Create Job Order")}
</Button>
</Stack>
<SearchBox
criteria={searchCriteria}
onSearch={onSearch}
@@ -139,6 +174,12 @@ const JoSearch: React.FC<Props> = ({ defaultInputs }) => {
totalCount={totalCount}
// isAutoPaging={false}
/>
<JoCreateFormModal
open={isCreateJoModalOpen}
bomCombo={bomCombo}
onClose={onCloseCreateJoModal}
onSearch={searchDataByPage}
/>
</>
}


+ 8
- 1
src/components/JoSearch/JoSearchWrapper.tsx Ver arquivo

@@ -2,6 +2,7 @@ import React from "react";
import GeneralLoading from "../General/GeneralLoading";
import JoSearch from "./JoSearch";
import { SearchJoResultRequest } from "@/app/api/jo/actions";
import { fetchBomCombo } from "@/app/api/bom";

interface SubComponents {
Loading: typeof GeneralLoading;
@@ -12,8 +13,14 @@ const JoSearchWrapper: React.FC & SubComponents = async () => {
code: "",
name: "",
}

const [
bomCombo
] = await Promise.all([
fetchBomCombo()
])
return <JoSearch defaultInputs={defaultInputs}/>
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo}/>
}

JoSearchWrapper.Loading = GeneralLoading;


Carregando…
Cancelar
Salvar