@@ -19,7 +19,24 @@ | |||||
"@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
"@mui/x-data-grid": "^6.18.7", | "@mui/x-data-grid": "^6.18.7", | ||||
"@mui/x-date-pickers": "^6.18.7", | "@mui/x-date-pickers": "^6.18.7", | ||||
"@tiptap/react": "^2.12.0", | |||||
"@tiptap/core": "^2.14.0", | |||||
"@tiptap/extension-color": "^2.14.0", | |||||
"@tiptap/extension-document": "^2.14.0", | |||||
"@tiptap/extension-gapcursor": "^2.14.0", | |||||
"@tiptap/extension-highlight": "^2.14.0", | |||||
"@tiptap/extension-list-item": "^2.14.0", | |||||
"@tiptap/extension-paragraph": "^2.14.0", | |||||
"@tiptap/extension-table": "^2.14.0", | |||||
"@tiptap/extension-table-cell": "^2.14.0", | |||||
"@tiptap/extension-table-header": "^2.14.0", | |||||
"@tiptap/extension-table-row": "^2.14.0", | |||||
"@tiptap/extension-text": "^2.14.0", | |||||
"@tiptap/extension-text-align": "^2.14.0", | |||||
"@tiptap/extension-text-style": "^2.14.0", | |||||
"@tiptap/extension-underline": "^2.14.0", | |||||
"@tiptap/pm": "^2.14.0", | |||||
"@tiptap/react": "^2.14.0", | |||||
"@tiptap/starter-kit": "^2.14.0", | |||||
"@unly/universal-language-detector": "^2.0.3", | "@unly/universal-language-detector": "^2.0.3", | ||||
"apexcharts": "^3.45.2", | "apexcharts": "^3.45.2", | ||||
"axios": "^1.9.0", | "axios": "^1.9.0", | ||||
@@ -28,6 +45,7 @@ | |||||
"i18next": "^23.7.11", | "i18next": "^23.7.11", | ||||
"i18next-resources-to-backend": "^1.2.0", | "i18next-resources-to-backend": "^1.2.0", | ||||
"lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
"mui-color-input": "^7.0.0", | |||||
"next": "14.0.4", | "next": "14.0.4", | ||||
"next-auth": "^4.24.5", | "next-auth": "^4.24.5", | ||||
"next-pwa": "^5.6.0", | "next-pwa": "^5.6.0", | ||||
@@ -0,0 +1,30 @@ | |||||
import ImportTesting from "@/components/ImportTesting"; | |||||
import { getServerI18n } from "@/i18n"; | |||||
import { Stack } from "@mui/material"; | |||||
import { Metadata } from "next"; | |||||
import React, { Suspense } from "react"; | |||||
export const metadata: Metadata = { | |||||
title: "Import Testing" | |||||
} | |||||
const ImportTestingPage: React.FC = async () => { | |||||
const { t } = await getServerI18n("importTesting"); | |||||
return ( | |||||
<> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
</Stack> | |||||
<Suspense fallback={<ImportTesting.Loading />}> | |||||
<ImportTesting /> | |||||
</Suspense> | |||||
</> | |||||
) | |||||
} | |||||
export default ImportTestingPage; |
@@ -0,0 +1,21 @@ | |||||
"use server"; | |||||
import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; | |||||
import { BASE_API_URL } from "@/config/api"; | |||||
export interface ImportPoForm { | |||||
dateFrom: string, | |||||
dateTo: string, | |||||
} | |||||
export interface ImportTestingForm { | |||||
po: ImportPoForm | |||||
} | |||||
export const testImportPo = async (data: ImportPoForm) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/m18/po`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}) | |||||
} |
@@ -0,0 +1 @@ | |||||
// "server only" |
@@ -49,6 +49,11 @@ export const dateStringToDayjs = (date: string) => { | |||||
return dayjs(date, OUTPUT_DATE_FORMAT) | return dayjs(date, OUTPUT_DATE_FORMAT) | ||||
} | } | ||||
export const dateTimeStringToDayjs = (dateTime: string) => { | |||||
// Format: YYYY/MM/DD HH:mm:ss | |||||
return dayjs(dateTime, `${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`) | |||||
} | |||||
export const stockInLineStatusMap: { [status: string]: number } = { | export const stockInLineStatusMap: { [status: string]: number } = { | ||||
"draft": 0, | "draft": 0, | ||||
"pending": 1, | "pending": 1, | ||||
@@ -19,6 +19,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||||
"/scheduling/detail": "Detail Scheduling", | "/scheduling/detail": "Detail Scheduling", | ||||
"/scheduling/detail/edit": "FG Production Schedule", | "/scheduling/detail/edit": "FG Production Schedule", | ||||
"/inventory": "Inventory", | "/inventory": "Inventory", | ||||
"/settings/importTesting": "Import Testing", | |||||
}; | }; | ||||
const Breadcrumb = () => { | const Breadcrumb = () => { | ||||
@@ -0,0 +1,127 @@ | |||||
"use client" | |||||
import { ImportTestingForm } from "@/app/api/settings/importTesting/actions"; | |||||
import { ImportPoForm } from "@/app/api/settings/importTesting/actions"; | |||||
import { INPUT_DATE_FORMAT, OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateTimeStringToDayjs } from "@/app/utils/formatUtil"; | |||||
import { Check } from "@mui/icons-material"; | |||||
import { Box, Button, Card, CardContent, FormControl, Grid, Stack, 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 React, { useCallback, useState } from "react"; | |||||
import { Controller, useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
interface Props { | |||||
} | |||||
const ImportPo: React.FC<Props> = ({ | |||||
}) => { | |||||
const { t } = useTranslation() | |||||
const [isLoading, setIsLoading] = useState(false) | |||||
const { | |||||
control, | |||||
formState: { errors }, | |||||
watch | |||||
} = useFormContext<ImportTestingForm>() | |||||
const handleDateTimePickerOnChange = useCallback((value: Dayjs | null, onChange: (value: any) => void) => { | |||||
const formattedValue = value ? value.format(`${INPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`) : null | |||||
onChange(formattedValue) | |||||
}, []) | |||||
return ( | |||||
<Card sx={{ width: '100%' }}> | |||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
<Grid container> | |||||
<Grid container> | |||||
<Typography variant="overline">{t("Import PO")}</Typography> | |||||
</Grid> | |||||
<Grid item xs={8}> | |||||
<LocalizationProvider | |||||
dateAdapter={AdapterDayjs} | |||||
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD | |||||
adapterLocale="zh-hk" | |||||
> | |||||
<Box display="flex"> | |||||
<Controller | |||||
control={control} | |||||
name="po.dateFrom" | |||||
rules={{ | |||||
required: "Please input the date From!", | |||||
validate: { | |||||
isValid: (value) => | |||||
value && dateTimeStringToDayjs(value).isValid() ? true : "Invalid date-time" | |||||
}, | |||||
}} | |||||
render={({ field, fieldState: { error } }) => ( | |||||
<DateTimePicker | |||||
label={t("Import Po From")} | |||||
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||||
onChange={(newValue: Dayjs | null) => (handleDateTimePickerOnChange(newValue, field.onChange))} | |||||
slotProps={{ | |||||
textField: { | |||||
error: !!error, | |||||
helperText: error ? error.message : null | |||||
} | |||||
}} | |||||
/> | |||||
)} | |||||
/> | |||||
<Box | |||||
display="flex" | |||||
alignItems="center" | |||||
justifyContent="center" | |||||
marginInline={2} | |||||
> | |||||
{"-"} | |||||
</Box> | |||||
<Controller | |||||
control={control} | |||||
name="po.dateTo" | |||||
rules={{ | |||||
required: "Please input the date to!", | |||||
validate: { | |||||
isValid: (value) => | |||||
value && dateTimeStringToDayjs(value).isValid() ? true : "Invalid date-time", | |||||
isFuture: (value) => | |||||
dateTimeStringToDayjs(value).isAfter(watch("po.dateFrom")) || "Date must be in the future", | |||||
}, | |||||
}} | |||||
render={({ field, fieldState: { error } }) => ( | |||||
<DateTimePicker | |||||
label={t("Import Po To")} | |||||
format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`} | |||||
onChange={(newValue: Dayjs | null) => (handleDateTimePickerOnChange(newValue, field.onChange))} | |||||
slotProps={{ | |||||
textField: { | |||||
error: !!error, | |||||
helperText: error ? error.message : null | |||||
} | |||||
}} | |||||
/> | |||||
)} | |||||
/> | |||||
</Box> | |||||
</LocalizationProvider> | |||||
</Grid> | |||||
</Grid> | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
<Button | |||||
name="importPo" | |||||
id="importPo" | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
> | |||||
{t("Import Po")} | |||||
</Button> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
) | |||||
} | |||||
export default ImportPo; |
@@ -0,0 +1,67 @@ | |||||
"use client" | |||||
import { ImportTestingForm, testImportPo } from "@/app/api/settings/importTesting/actions"; | |||||
import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; | |||||
import React, { BaseSyntheticEvent, FormEvent, useCallback, useState } from "react"; | |||||
import { FormProvider, SubmitErrorHandler, useForm } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
import ImportPo from "./ImportPo"; | |||||
import { ImportPoForm } from "@/app/api/settings/importTesting/actions"; | |||||
interface Props { | |||||
} | |||||
const ImportTesting: React.FC<Props> = ({ | |||||
}) => { | |||||
const { t } = useTranslation() | |||||
const [isLoading, setIsLoading] = useState(false) | |||||
const formProps = useForm<ImportTestingForm>() | |||||
const onSubmit = useCallback(async (data: ImportTestingForm, event?: BaseSyntheticEvent) => { | |||||
const buttonId = (event?.nativeEvent as SubmitEvent).submitter?.id | |||||
console.log(data.po) | |||||
switch (buttonId) { | |||||
case "importPo": | |||||
setIsLoading(() => true) | |||||
const response = await testImportPo(data.po) | |||||
console.log(response) | |||||
if (response) { | |||||
setIsLoading(() => false) | |||||
} | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
}, []) | |||||
const onSubmitError = useCallback<SubmitErrorHandler<ImportTestingForm>>( | |||||
(errors) => { | |||||
console.log(errors) | |||||
}, | |||||
[], | |||||
); | |||||
return ( | |||||
<Card> | |||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
<Typography variant="overline">{t("Status: ")}{isLoading ? t("Importing...") : t("Ready to import")}</Typography> | |||||
<FormProvider {...formProps}> | |||||
<Stack | |||||
spacing={2} | |||||
component={"form"} | |||||
onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
> | |||||
<Grid container> | |||||
<ImportPo /> | |||||
</Grid> | |||||
</Stack> | |||||
</FormProvider> | |||||
</CardContent> | |||||
</Card> | |||||
) | |||||
} | |||||
export default ImportTesting; |
@@ -0,0 +1,17 @@ | |||||
import React from "react"; | |||||
import GeneralLoading from "../General/GeneralLoading" | |||||
import ImportTesting from "./ImportTesting"; | |||||
interface SubComponents { | |||||
Loading: typeof GeneralLoading; | |||||
} | |||||
const ImportTestingWrapper: React.FC & SubComponents = async () => { | |||||
return <ImportTesting/> | |||||
} | |||||
ImportTestingWrapper.Loading = GeneralLoading; | |||||
export default ImportTestingWrapper |
@@ -0,0 +1 @@ | |||||
export { default } from './ImportTestingWrapper' |
@@ -263,6 +263,11 @@ const NavigationContent: React.FC = () => { | |||||
label: "Mail", | label: "Mail", | ||||
path: "/settings/mail", | path: "/settings/mail", | ||||
}, | }, | ||||
{ | |||||
icon: <RequestQuote />, | |||||
label: "Import Testing", | |||||
path: "/settings/importTesting", | |||||
}, | |||||
], | ], | ||||
}, | }, | ||||
]; | ]; | ||||