@@ -1,6 +1,6 @@ | |||||
import { preloadInventory } from "@/app/api/inventory"; | import { preloadInventory } from "@/app/api/inventory"; | ||||
import InventorySearch from "@/components/InventorySearch"; | import InventorySearch from "@/components/InventorySearch"; | ||||
import { getServerI18n } from "@/i18n"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import { Stack, Typography } from "@mui/material"; | import { Stack, Typography } from "@mui/material"; | ||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
@@ -10,7 +10,7 @@ export const metadata: Metadata = { | |||||
} | } | ||||
const Inventory: React.FC = async () => { | const Inventory: React.FC = async () => { | ||||
const { t } = await getServerI18n("inventory") | |||||
const { t } = await getServerI18n("inventory", "common") | |||||
preloadInventory() | preloadInventory() | ||||
@@ -25,9 +25,11 @@ const Inventory: React.FC = async () => { | |||||
{t("Inventory")} | {t("Inventory")} | ||||
</Typography> | </Typography> | ||||
</Stack> | </Stack> | ||||
<Suspense fallback={<InventorySearch.Loading />}> | |||||
<InventorySearch /> | |||||
</Suspense> | |||||
<I18nProvider namespaces={["common", "inventory"]}> | |||||
<Suspense fallback={<InventorySearch.Loading />}> | |||||
<InventorySearch /> | |||||
</Suspense> | |||||
</I18nProvider> | |||||
</>; | </>; | ||||
} | } | ||||
@@ -16,29 +16,20 @@ export const metadata: Metadata = { | |||||
const PurchaseOrder: React.FC = async () => { | const PurchaseOrder: React.FC = async () => { | ||||
const { t } = await getServerI18n("purchaseOrder"); | const { t } = await getServerI18n("purchaseOrder"); | ||||
// preloadClaims(); | |||||
// preloadClaims(); | |||||
return ( | return ( | ||||
<> | <> | ||||
<I18nProvider namespaces={["purchaseOrder", "common"]}> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | |||||
{/* <Button | |||||
variant="contained" | |||||
startIcon={<Add />} | |||||
LinkComponent={Link} | |||||
href="/po/create" | |||||
<I18nProvider namespaces={["purchaseOrder", "common"]}> | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
flexWrap="wrap" | |||||
rowGap={2} | |||||
> | > | ||||
{t("Create Po")} | |||||
</Button> */} | |||||
</Stack> | |||||
<Suspense fallback={<PoSearch.Loading />}> | |||||
<PoSearch /> | |||||
</Suspense> | |||||
</Stack> | |||||
<Suspense fallback={<PoSearch.Loading />}> | |||||
<PoSearch /> | |||||
</Suspense> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -11,9 +11,9 @@ export interface InventoryResult { | |||||
qty: number; | qty: number; | ||||
uomCode: string; | uomCode: string; | ||||
uomUdfudesc: string; | uomUdfudesc: string; | ||||
germPerSmallestUnit: number; | |||||
qtyPerSmallestUnit: number; | |||||
smallestUnit: string; | |||||
// germPerSmallestUnit: number; | |||||
// qtyPerSmallestUnit: number; | |||||
// smallestUnit: string; | |||||
price: number; | price: number; | ||||
currencyName: string; | currencyName: string; | ||||
status: string; | status: string; | ||||
@@ -8,6 +8,11 @@ export interface M18ImportPoForm { | |||||
modifiedDateTo: string, | modifiedDateTo: string, | ||||
} | } | ||||
export interface M18ImportDoForm { | |||||
modifiedDateFrom: string, | |||||
modifiedDateTo: string, | |||||
} | |||||
export interface M18ImportPqForm { | export interface M18ImportPqForm { | ||||
modifiedDateFrom: string, | modifiedDateFrom: string, | ||||
modifiedDateTo: string, | modifiedDateTo: string, | ||||
@@ -20,6 +25,7 @@ export interface M18ImportMasterDataForm { | |||||
export interface M18ImportTestingForm { | export interface M18ImportTestingForm { | ||||
po: M18ImportPoForm, | po: M18ImportPoForm, | ||||
do: M18ImportDoForm, | |||||
pq: M18ImportPqForm, | pq: M18ImportPqForm, | ||||
masterData: M18ImportMasterDataForm, | masterData: M18ImportMasterDataForm, | ||||
} | } | ||||
@@ -32,6 +38,14 @@ export const testM18ImportPo = async (data: M18ImportPoForm) => { | |||||
}) | }) | ||||
} | } | ||||
export const testM18ImportDo = async (data: M18ImportDoForm) => { | |||||
return serverFetchWithNoContent(`${BASE_API_URL}/m18/do`, { | |||||
method: "POST", | |||||
body: JSON.stringify(data), | |||||
headers: { "Content-Type": "application/json" }, | |||||
}) | |||||
} | |||||
export const testM18ImportPq = async (data: M18ImportPqForm) => { | export const testM18ImportPq = async (data: M18ImportPqForm) => { | ||||
return serverFetchWithNoContent(`${BASE_API_URL}/m18/pq`, { | return serverFetchWithNoContent(`${BASE_API_URL}/m18/pq`, { | ||||
method: "POST", | method: "POST", | ||||
@@ -27,7 +27,7 @@ type SearchParamNames = keyof SearchQuery; | |||||
const InventorySearch: React.FC<Props> = ({ | const InventorySearch: React.FC<Props> = ({ | ||||
inventories, | inventories, | ||||
}) => { | }) => { | ||||
const { t } = useTranslation("inventories"); | |||||
const { t } = useTranslation(["inventory", "common"]); | |||||
const [filteredInventories, setFilteredInventories] = useState(inventories) | const [filteredInventories, setFilteredInventories] = useState(inventories) | ||||
@@ -56,6 +56,9 @@ const InventorySearch: React.FC<Props> = ({ | |||||
{ | { | ||||
name: "type", | name: "type", | ||||
label: t("Type"), | label: t("Type"), | ||||
renderCell: (params) => { | |||||
return t(params.type) | |||||
} | |||||
}, | }, | ||||
{ | { | ||||
name: "qty", | name: "qty", | ||||
@@ -68,17 +71,17 @@ const InventorySearch: React.FC<Props> = ({ | |||||
name: "uomUdfudesc", | name: "uomUdfudesc", | ||||
label: t("UoM"), | label: t("UoM"), | ||||
}, | }, | ||||
{ | |||||
name: "qtyPerSmallestUnit", | |||||
label: t("Qty Per Smallest Unit"), | |||||
align: "right", | |||||
headerAlign: "right", | |||||
type: "decimal" | |||||
}, | |||||
{ | |||||
name: "smallestUnit", | |||||
label: t("Smallest Unit"), | |||||
}, | |||||
// { | |||||
// name: "qtyPerSmallestUnit", | |||||
// label: t("Qty Per Smallest Unit"), | |||||
// align: "right", | |||||
// headerAlign: "right", | |||||
// type: "decimal" | |||||
// }, | |||||
// { | |||||
// name: "smallestUnit", | |||||
// label: t("Smallest Unit"), | |||||
// }, | |||||
// { | // { | ||||
// name: "price", | // name: "price", | ||||
// label: t("Price"), | // label: t("Price"), | ||||
@@ -0,0 +1,126 @@ | |||||
"use client" | |||||
import { M18ImportTestingForm, M18ImportDoForm } from "@/app/api/settings/m18ImportTesting/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 M18ImportDo: React.FC<Props> = ({ | |||||
}) => { | |||||
const { t } = useTranslation() | |||||
const [isLoading, setIsLoading] = useState(false) | |||||
const { | |||||
control, | |||||
formState: { errors }, | |||||
watch | |||||
} = useFormContext<M18ImportTestingForm>() | |||||
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 Delivery Order")}</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="do.modifiedDateFrom" | |||||
// 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("Modified Date 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="do.modifiedDateTo" | |||||
// rules={{ | |||||
// required: "Please input the date to!", | |||||
// validate: { | |||||
// isValid: (value) => | |||||
// value && dateTimeStringToDayjs(value).isValid() ? true : "Invalid date-time", | |||||
// isFuture: (value) => | |||||
// dateTimeStringToDayjs(value).isAfter(watch("do.dateFrom")) || "Date must be in the future", | |||||
// }, | |||||
// }} | |||||
render={({ field, fieldState: { error } }) => ( | |||||
<DateTimePicker | |||||
label={t("Modified Date 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="m18ImportDo" | |||||
id="m18ImportDo" | |||||
variant="contained" | |||||
startIcon={<Check />} | |||||
type="submit" | |||||
> | |||||
{t("Import Do")} | |||||
</Button> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
) | |||||
} | |||||
export default M18ImportDo; |
@@ -1,6 +1,6 @@ | |||||
"use client" | "use client" | ||||
import { M18ImportTestingForm, testM18ImportPo, M18ImportPoForm, testM18ImportPq, testM18ImportMasterData } from "@/app/api/settings/m18ImportTesting/actions"; | |||||
import { M18ImportTestingForm, testM18ImportPo, M18ImportPoForm, testM18ImportPq, testM18ImportMasterData, testM18ImportDo } from "@/app/api/settings/m18ImportTesting/actions"; | |||||
import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; | import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; | ||||
import React, { BaseSyntheticEvent, FormEvent, useCallback, useState } from "react"; | import React, { BaseSyntheticEvent, FormEvent, useCallback, useState } from "react"; | ||||
import { FormProvider, SubmitErrorHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitErrorHandler, useForm } from "react-hook-form"; | ||||
@@ -9,6 +9,7 @@ import M18ImportPo from "./M18ImportPo"; | |||||
import M18ImportPq from "./M18ImportPq"; | import M18ImportPq from "./M18ImportPq"; | ||||
import { dateTimeStringToDayjs } from "@/app/utils/formatUtil"; | import { dateTimeStringToDayjs } from "@/app/utils/formatUtil"; | ||||
import M18ImportMasterData from "./M18ImportMasterData"; | import M18ImportMasterData from "./M18ImportMasterData"; | ||||
import M18ImportDo from "./M18ImportDo"; | |||||
interface Props { | interface Props { | ||||
@@ -20,6 +21,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
const { t } = useTranslation() | const { t } = useTranslation() | ||||
const [isLoading, setIsLoading] = useState(false) | const [isLoading, setIsLoading] = useState(false) | ||||
const [loadingType, setLoadingType] = useState<String | null>(null) | |||||
const formProps = useForm<M18ImportTestingForm>() | const formProps = useForm<M18ImportTestingForm>() | ||||
const onSubmit = useCallback(async (data: M18ImportTestingForm, event?: BaseSyntheticEvent) => { | const onSubmit = useCallback(async (data: M18ImportTestingForm, event?: BaseSyntheticEvent) => { | ||||
@@ -37,11 +39,11 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
case "m18ImportMasterData": | case "m18ImportMasterData": | ||||
const mdDateFrom = data.masterData.modifiedDateFrom | const mdDateFrom = data.masterData.modifiedDateFrom | ||||
const mdDateTo = data.masterData.modifiedDateTo | const mdDateTo = data.masterData.modifiedDateTo | ||||
if (!(!!mdDateFrom && dateTimeStringToDayjs(mdDateFrom).isValid())) { | |||||
if (!(!mdDateFrom || dateTimeStringToDayjs(mdDateFrom).isValid())) { | |||||
formProps.setError("masterData.modifiedDateFrom", { message: "Invalid DateTime Format" }) | formProps.setError("masterData.modifiedDateFrom", { message: "Invalid DateTime Format" }) | ||||
} | } | ||||
if (!(!!mdDateTo && dateTimeStringToDayjs(mdDateTo).isValid())) { | |||||
if (!(!mdDateTo || dateTimeStringToDayjs(mdDateTo).isValid())) { | |||||
formProps.setError("masterData.modifiedDateTo", { message: "Invalid DateTime Format" }) | formProps.setError("masterData.modifiedDateTo", { message: "Invalid DateTime Format" }) | ||||
} | } | ||||
@@ -50,6 +52,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
} | } | ||||
setIsLoading(() => true) | setIsLoading(() => true) | ||||
setLoadingType(() => "Master Data") | |||||
const mdResponse = await testM18ImportMasterData(data.masterData) | const mdResponse = await testM18ImportMasterData(data.masterData) | ||||
console.log(mdResponse) | console.log(mdResponse) | ||||
if (mdResponse) { | if (mdResponse) { | ||||
@@ -72,20 +75,44 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
} | } | ||||
setIsLoading(() => true) | setIsLoading(() => true) | ||||
const poResponse = await testM18ImportMasterData(data.po) | |||||
setLoadingType(() => "Purchase Order") | |||||
const poResponse = await testM18ImportPo(data.po) | |||||
console.log(poResponse) | console.log(poResponse) | ||||
if (poResponse) { | if (poResponse) { | ||||
setIsLoading(() => false) | setIsLoading(() => false) | ||||
} | } | ||||
break; | break; | ||||
case "m18ImportDo": | |||||
const doDateFrom = data.do.modifiedDateFrom | |||||
const doDateTo = data.do.modifiedDateTo | |||||
if (!(doDateFrom && dateTimeStringToDayjs(doDateFrom).isValid())) { | |||||
formProps.setError("do.modifiedDateFrom", { message: "Invalid DateTime Format" }) | |||||
} | |||||
if (!(doDateTo && dateTimeStringToDayjs(doDateTo).isValid())) { | |||||
formProps.setError("do.modifiedDateTo", { message: "Invalid DateTime Format" }) | |||||
} | |||||
if (formProps.formState.errors.do) { | |||||
return; | |||||
} | |||||
setIsLoading(() => true) | |||||
setLoadingType(() => "Delivery Order") | |||||
const doResponse = await testM18ImportDo(data.po) | |||||
console.log(doResponse) | |||||
if (doResponse) { | |||||
setIsLoading(() => false) | |||||
} | |||||
break; | |||||
case "m18ImportPq": | case "m18ImportPq": | ||||
const pqDateFrom = data.pq.modifiedDateFrom | const pqDateFrom = data.pq.modifiedDateFrom | ||||
const pqDateTo = data.pq.modifiedDateTo | const pqDateTo = data.pq.modifiedDateTo | ||||
if (!(!!pqDateFrom && dateTimeStringToDayjs(pqDateFrom).isValid())) { | |||||
if (!(pqDateFrom || dateTimeStringToDayjs(pqDateFrom).isValid())) { | |||||
formProps.setError("pq.modifiedDateFrom", { message: "Invalid DateTime Format" }) | formProps.setError("pq.modifiedDateFrom", { message: "Invalid DateTime Format" }) | ||||
} | } | ||||
if (!(!!pqDateTo && dateTimeStringToDayjs(pqDateTo).isValid())) { | |||||
if (!(pqDateTo || dateTimeStringToDayjs(pqDateTo).isValid())) { | |||||
formProps.setError("pq.modifiedDateTo", { message: "Invalid DateTime Format" }) | formProps.setError("pq.modifiedDateTo", { message: "Invalid DateTime Format" }) | ||||
} | } | ||||
@@ -94,6 +121,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
} | } | ||||
setIsLoading(() => true) | setIsLoading(() => true) | ||||
setLoadingType(() => "Purchase Quotation") | |||||
const pqResponse = await testM18ImportPq(data.pq) | const pqResponse = await testM18ImportPq(data.pq) | ||||
console.log(pqResponse) | console.log(pqResponse) | ||||
if (pqResponse) { | if (pqResponse) { | ||||
@@ -115,7 +143,7 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
return ( | return ( | ||||
<Card> | <Card> | ||||
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | ||||
<Typography variant="overline">{t("Status: ")}{isLoading ? t("Importing...") : t("Ready to import")}</Typography> | |||||
<Typography variant="overline">{t("Status: ")}{isLoading ? t(`Importing ${loadingType}...`) : t("Ready to import")}</Typography> | |||||
<FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
<Stack | <Stack | ||||
spacing={2} | spacing={2} | ||||
@@ -129,6 +157,9 @@ const M18ImportTesting: React.FC<Props> = ({ | |||||
<Grid item xs={12}> | <Grid item xs={12}> | ||||
<M18ImportPo /> | <M18ImportPo /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={12}> | |||||
<M18ImportDo /> | |||||
</Grid> | |||||
<Grid item xs={12}> | <Grid item xs={12}> | ||||
<M18ImportPq /> | <M18ImportPq /> | ||||
</Grid> | </Grid> | ||||
@@ -382,7 +382,7 @@ function PoInputGrid({ | |||||
headerName: t("acceptedQty"), | headerName: t("acceptedQty"), | ||||
flex: 0.5, | flex: 0.5, | ||||
type: "number", | type: "number", | ||||
editable: true, | |||||
// editable: true, | |||||
// replace with tooltip + content | // replace with tooltip + content | ||||
}, | }, | ||||
{ | { | ||||
@@ -0,0 +1,3 @@ | |||||
{ | |||||
} |
@@ -3,5 +3,8 @@ | |||||
"All": "全部", | "All": "全部", | ||||
"No options": "沒有選項", | "No options": "沒有選項", | ||||
"Reset": "重置", | "Reset": "重置", | ||||
"Search": "搜尋" | |||||
"Search": "搜尋", | |||||
"Code": "編號", | |||||
"Name": "名稱", | |||||
"Type": "類型" | |||||
} | } |
@@ -0,0 +1,11 @@ | |||||
{ | |||||
"Inventory": "存貨", | |||||
"Code": "編號", | |||||
"Name": "名稱", | |||||
"Type": "類型", | |||||
"Status": "狀態", | |||||
"Qty": "數量", | |||||
"UoM": "單位", | |||||
"mat": "物料", | |||||
"fg": "成品" | |||||
} |