| @@ -0,0 +1,38 @@ | |||||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||||
| import DoDetail from "@/components/DoDetail/DodetailWrapper"; | |||||
| 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 Delivery Order Detail" | |||||
| } | |||||
| type Props = SearchParams; | |||||
| const DoEdit: React.FC<Props> = async ({ searchParams }) => { | |||||
| const { t } = await getServerI18n("do"); | |||||
| const id = searchParams["id"]; | |||||
| if (!id || isArray(id) || !isFinite(parseInt(id))) { | |||||
| notFound(); | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Edit Delivery Order Detail")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["do", "common"]}> | |||||
| <Suspense fallback={<DoDetail.Loading />}> | |||||
| <DoDetail id={parseInt(id)} /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default DoEdit; | |||||
| @@ -7,7 +7,7 @@ import { Metadata } from "next"; | |||||
| import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Pick Order", | |||||
| title: "Finished Good Order", | |||||
| }; | }; | ||||
| const PickOrder: React.FC = async () => { | const PickOrder: React.FC = async () => { | ||||
| @@ -12,3 +12,60 @@ import { GridRowId, GridRowSelectionModel } from "@mui/x-data-grid"; | |||||
| export interface CreateConsoDoInput { | export interface CreateConsoDoInput { | ||||
| ids: GridRowSelectionModel; | ids: GridRowSelectionModel; | ||||
| } | } | ||||
| export interface DoDetail { | |||||
| id: number; | |||||
| code: string; | |||||
| supplierCode: string; | |||||
| shopCode: string; | |||||
| currencyCode: string; | |||||
| orderDate: string; | |||||
| estimatedArrivalDate: string; | |||||
| completeDate: string; | |||||
| status: string; | |||||
| deliveryOrderLines: DoDetailLine[]; | |||||
| } | |||||
| export interface DoDetailLine { | |||||
| id: number; | |||||
| itemNo: string; | |||||
| qty: number; | |||||
| price: number; | |||||
| status: string; | |||||
| itemName?: string; | |||||
| uomCode?: string; | |||||
| } | |||||
| export interface ReleaseDoRequest { | |||||
| id: number; | |||||
| } | |||||
| export interface ReleaseDoResponse { | |||||
| id: number; | |||||
| entity: { status: string } | |||||
| } | |||||
| export const releaseDo = cache(async (data: ReleaseDoRequest) => { | |||||
| return await serverFetchJson<ReleaseDoResponse>(`${BASE_API_URL}/do/release`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }) | |||||
| }) | |||||
| export const preloadDo = () => { | |||||
| fetchDoList(); | |||||
| }; | |||||
| export const fetchDoList = cache(async () => { | |||||
| return serverFetchJson<DoResult[]>(`${BASE_API_URL}/do/list`, { | |||||
| next: { tags: ["doList"] }, | |||||
| }); | |||||
| }); | |||||
| export const fetchDoDetail = cache(async (id: number) => { | |||||
| return serverFetchJson<DoDetail>(`${BASE_API_URL}/do/detail/${id}`, { | |||||
| method: "GET", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| next: { | |||||
| tags: ["doDetail"] | |||||
| } | |||||
| }); | |||||
| }); | |||||
| @@ -1,3 +1,108 @@ | |||||
| "use client"; | "use client"; | ||||
| // const doDetail = | |||||
| import type { DoDetail as DoDetailType } from "@/app/api/do/actions"; | |||||
| 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 ArrowBackIcon from '@mui/icons-material/ArrowBack'; | |||||
| import StartIcon from "@mui/icons-material/Start"; | |||||
| import { releaseDo } from "@/app/api/do/actions"; | |||||
| import DoInfoCard from "./DoInfoCard"; | |||||
| import DoLineTable from "./DoLineTable"; | |||||
| type Props = { | |||||
| id?: number; | |||||
| defaultValues: Partial<DoDetailType> | undefined; | |||||
| } | |||||
| const DoDetail: React.FC<Props> = ({ | |||||
| defaultValues, | |||||
| id, | |||||
| }) => { | |||||
| const { t } = useTranslation("do") | |||||
| const router = useRouter(); | |||||
| const { setIsUploading } = useUploadContext(); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const formProps = useForm<DoDetailType>({ | |||||
| defaultValues: defaultValues | |||||
| }) | |||||
| const handleBack = useCallback(() => { | |||||
| router.replace(`/do`) | |||||
| }, []) | |||||
| const handleRelease = useCallback(async () => { | |||||
| try { | |||||
| setIsUploading(true) | |||||
| if (id) { | |||||
| console.log(id) | |||||
| const response = await releaseDo({ 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) | |||||
| } | |||||
| }, [id, formProps, t, setIsUploading]) | |||||
| const onSubmit = useCallback<SubmitHandler<DoDetailType>>(async (data, event) => { | |||||
| console.log(data) | |||||
| }, [t]) | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<DoDetailType>>((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() === "pending" && ( | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<StartIcon />} | |||||
| onClick={handleRelease} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| <DoInfoCard /> | |||||
| <DoLineTable /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<ArrowBackIcon />} | |||||
| onClick={handleBack} | |||||
| > | |||||
| {t("Back")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| </> | |||||
| } | |||||
| export default DoDetail; | |||||
| @@ -0,0 +1,26 @@ | |||||
| import React from "react"; | |||||
| import GeneralLoading from "../General/GeneralLoading"; | |||||
| import { fetchDoDetail } from "@/app/api/do/actions"; | |||||
| import DoDetail from "./DoDetail"; | |||||
| interface SubComponents { | |||||
| Loading: typeof GeneralLoading; | |||||
| } | |||||
| type DoDetailProps = { | |||||
| id?: number; | |||||
| } | |||||
| type Props = DoDetailProps | |||||
| const DoDetailWrapper: React.FC<Props> & SubComponents = async ({ | |||||
| id, | |||||
| }) => { | |||||
| const doDetail = id ? await fetchDoDetail(id) : undefined | |||||
| return <DoDetail id={id} defaultValues={doDetail}/> | |||||
| } | |||||
| DoDetailWrapper.Loading = GeneralLoading; | |||||
| export default DoDetailWrapper; | |||||
| @@ -0,0 +1,97 @@ | |||||
| import { DoDetail } from "@/app/api/do/actions"; | |||||
| 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 DoInfoCard: React.FC<Props> = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation("do"); | |||||
| const { control, getValues, register, watch } = useFormContext<DoDetail>(); | |||||
| 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 | |||||
| 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("supplierCode")} | |||||
| label={t("Supplier Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| {...register("shopCode")} | |||||
| label={t("Shop Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| {...register("currencyCode")} | |||||
| label={t("Currency Code")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| {...register("orderDate")} | |||||
| label={t("Order Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| {...register("estimatedArrivalDate")} | |||||
| label={t("Estimated Arrival Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| {...register("completeDate")} | |||||
| label={t("Complete Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}/> | |||||
| </Grid> | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ) | |||||
| } | |||||
| export default DoInfoCard; | |||||
| @@ -0,0 +1,98 @@ | |||||
| import { DoDetail } from "@/app/api/do/actions"; | |||||
| 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 DoLineTable: React.FC<Props> = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation("do") | |||||
| const { | |||||
| watch | |||||
| } = useFormContext<DoDetail>() | |||||
| const columns = useMemo<GridColDef[]>(() => [ | |||||
| { | |||||
| field: "itemNo", | |||||
| headerName: t("Item No."), | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: "itemName", | |||||
| headerName: t("Item Name"), | |||||
| flex: 1, | |||||
| renderCell: (row) => { | |||||
| return isEmpty(row.value) ? "N/A" : row.value | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "qty", | |||||
| headerName: t("Quantity"), | |||||
| flex: 1, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (row) => { | |||||
| return decimalFormatter.format(row.value) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "price", | |||||
| headerName: t("Price"), | |||||
| flex: 1, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (row) => { | |||||
| return decimalFormatter.format(row.value) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "uomCode", | |||||
| headerName: t("UoM"), | |||||
| flex: 1, | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| renderCell: (row) => { | |||||
| return isEmpty(row.value) ? "N/A" : row.value | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "status", | |||||
| headerName: t("Status"), | |||||
| flex: 1, | |||||
| renderCell: (row) => { | |||||
| return t(upperFirst(row.value)) | |||||
| }, | |||||
| }, | |||||
| ], [t]) | |||||
| 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("deliveryOrderLines")} | |||||
| columns={columns} | |||||
| /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default DoLineTable; | |||||
| @@ -0,0 +1,3 @@ | |||||
| export { default } from "./DodetailWrapper"; | |||||
| export { default as DoInfoCard } from './DoInfoCard'; | |||||
| export { default as DoLineTable } from './DoLineTable'; | |||||
| @@ -155,6 +155,22 @@ const DoSearch: React.FC<Props> = ({ dos }) => { | |||||
| // onClick: onDetailClick, | // onClick: onDetailClick, | ||||
| // buttonIcon: <EditNote />, | // buttonIcon: <EditNote />, | ||||
| // }, | // }, | ||||
| { | |||||
| field: "id", | |||||
| headerName: t("Details"), | |||||
| width: 100, | |||||
| renderCell: (params) => ( | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| startIcon={<EditNote />} | |||||
| onClick={() => onDetailClick(params.row)} | |||||
| > | |||||
| {t("Details")} | |||||
| </Button> | |||||
| ), | |||||
| }, | |||||
| { | { | ||||
| field: "code", | field: "code", | ||||
| headerName: t("code"), | headerName: t("code"), | ||||
| @@ -85,13 +85,14 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell>{t("Lot No")}</TableCell> | <TableCell>{t("Lot No")}</TableCell> | ||||
| <TableCell>{t("Expiry Date")}</TableCell> | |||||
| {/* <TableCell>{t("Expiry Date")}</TableCell> */} | |||||
| <TableCell>{t("Location")}</TableCell> | <TableCell>{t("Location")}</TableCell> | ||||
| <TableCell align="right">{t("Current Stock")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Qty Already Picked")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell>{t("Stock Unit")}</TableCell> | <TableCell>{t("Stock Unit")}</TableCell> | ||||
| <TableCell align="right">{t("Available Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Required Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Actual Pick Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Pick Qty")}</TableCell> | |||||
| <TableCell align="center">{t("Submit")}</TableCell> | <TableCell align="center">{t("Submit")}</TableCell> | ||||
| <TableCell align="center">{t("Reject")}</TableCell> | <TableCell align="center">{t("Reject")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -128,11 +129,12 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||||
| <TableCell sx={{ color: textColor }}>{lot.itemCode}</TableCell> | <TableCell sx={{ color: textColor }}>{lot.itemCode}</TableCell> | ||||
| <TableCell sx={{ color: textColor }}>{lot.itemName}</TableCell> | <TableCell sx={{ color: textColor }}>{lot.itemName}</TableCell> | ||||
| <TableCell sx={{ color: textColor }}>{lot.lotNo}</TableCell> | <TableCell sx={{ color: textColor }}>{lot.lotNo}</TableCell> | ||||
| <TableCell sx={{ color: textColor }}> | |||||
| {/* <TableCell sx={{ color: textColor }}> | |||||
| {lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A'} | {lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A'} | ||||
| </TableCell> | </TableCell> | ||||
| */} | |||||
| <TableCell sx={{ color: textColor }}>{lot.location}</TableCell> | <TableCell sx={{ color: textColor }}>{lot.location}</TableCell> | ||||
| <TableCell sx={{ color: textColor }}>{lot.stockUnit}</TableCell> | |||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.availableQty}</TableCell> | <TableCell align="right" sx={{ color: textColor }}>{lot.availableQty}</TableCell> | ||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.requiredQty}</TableCell> | <TableCell align="right" sx={{ color: textColor }}>{lot.requiredQty}</TableCell> | ||||
| <TableCell align="right" sx={{ color: textColor }}>{lot.actualPickQty || 0}</TableCell> | <TableCell align="right" sx={{ color: textColor }}>{lot.actualPickQty || 0}</TableCell> | ||||
| @@ -165,6 +167,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||||
| }} | }} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| <TableCell sx={{ color: textColor }}>{lot.stockUnit}</TableCell> | |||||
| <TableCell align="center"> | <TableCell align="center"> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| @@ -245,7 +245,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| <Grid container> | <Grid container> | ||||
| <Grid item xs={8}> | <Grid item xs={8}> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| {t("Pick Order")} | |||||
| {t("Finished Good Order")} | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| {/* | {/* | ||||
| @@ -462,8 +462,8 @@ const LotTable: React.FC<LotTableProps> = ({ | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | ||||
| <TableCell>{t("Stock Unit")}</TableCell> | <TableCell>{t("Stock Unit")}</TableCell> | ||||
| <TableCell align="center">{t("QR Code Scan")}</TableCell> | <TableCell align="center">{t("QR Code Scan")}</TableCell> | ||||
| <TableCell align="center">{t("QC Check")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Reject")}</TableCell> | |||||
| <TableCell align="center">{t("Submit")}</TableCell> | <TableCell align="center">{t("Submit")}</TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| @@ -188,5 +188,6 @@ | |||||
| "Expiry Date": "到期日", | "Expiry Date": "到期日", | ||||
| "Location": "位置", | "Location": "位置", | ||||
| "All Pick Order Lots": "所有提料單批次", | "All Pick Order Lots": "所有提料單批次", | ||||
| "Completed": "已完成" | |||||
| "Completed": "已完成", | |||||
| "Finished Good Order": "成品訂單" | |||||
| } | } | ||||