| @@ -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"; | |||
| export const metadata: Metadata = { | |||
| title: "Pick Order", | |||
| title: "Finished Good Order", | |||
| }; | |||
| const PickOrder: React.FC = async () => { | |||
| @@ -12,3 +12,60 @@ import { GridRowId, GridRowSelectionModel } from "@mui/x-data-grid"; | |||
| export interface CreateConsoDoInput { | |||
| 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"; | |||
| // 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, | |||
| // 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", | |||
| headerName: t("code"), | |||
| @@ -85,13 +85,14 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell>{t("Expiry Date")}</TableCell> | |||
| {/* <TableCell>{t("Expiry Date")}</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 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("Reject")}</TableCell> | |||
| </TableRow> | |||
| @@ -128,11 +129,12 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| <TableCell sx={{ color: textColor }}>{lot.itemCode}</TableCell> | |||
| <TableCell sx={{ color: textColor }}>{lot.itemName}</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'} | |||
| </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.requiredQty}</TableCell> | |||
| <TableCell align="right" sx={{ color: textColor }}>{lot.actualPickQty || 0}</TableCell> | |||
| @@ -165,6 +167,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({ | |||
| }} | |||
| /> | |||
| </TableCell> | |||
| <TableCell sx={{ color: textColor }}>{lot.stockUnit}</TableCell> | |||
| <TableCell align="center"> | |||
| <Button | |||
| variant="contained" | |||
| @@ -245,7 +245,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| <Grid container> | |||
| <Grid item xs={8}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Pick Order")} | |||
| {t("Finished Good Order")} | |||
| </Typography> | |||
| </Grid> | |||
| {/* | |||
| @@ -462,8 +462,8 @@ const LotTable: React.FC<LotTableProps> = ({ | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| <TableCell>{t("Stock Unit")}</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> | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -188,5 +188,6 @@ | |||
| "Expiry Date": "到期日", | |||
| "Location": "位置", | |||
| "All Pick Order Lots": "所有提料單批次", | |||
| "Completed": "已完成" | |||
| "Completed": "已完成", | |||
| "Finished Good Order": "成品訂單" | |||
| } | |||