| @@ -19,7 +19,24 @@ | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| "@mui/x-data-grid": "^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", | |||
| "apexcharts": "^3.45.2", | |||
| "axios": "^1.9.0", | |||
| @@ -28,6 +45,7 @@ | |||
| "i18next": "^23.7.11", | |||
| "i18next-resources-to-backend": "^1.2.0", | |||
| "lodash": "^4.17.21", | |||
| "mui-color-input": "^7.0.0", | |||
| "next": "14.0.4", | |||
| "next-auth": "^4.24.5", | |||
| "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) | |||
| } | |||
| 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 } = { | |||
| "draft": 0, | |||
| "pending": 1, | |||
| @@ -19,6 +19,7 @@ const pathToLabelMap: { [path: string]: string } = { | |||
| "/scheduling/detail": "Detail Scheduling", | |||
| "/scheduling/detail/edit": "FG Production Schedule", | |||
| "/inventory": "Inventory", | |||
| "/settings/importTesting": "Import Testing", | |||
| }; | |||
| 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", | |||
| path: "/settings/mail", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Import Testing", | |||
| path: "/settings/importTesting", | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||