From 198469f6d52306fb6ee77315d243c4f5f5877e88 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 7 Jul 2025 18:33:56 +0800 Subject: [PATCH] update pick order --- src/app/api/pickOrder/actions.ts | 73 +- src/app/api/qc/actions.ts | 16 +- src/app/utils/formatUtil.ts | 7 +- src/app/utils/gridUtil.tsx | 26 + src/components/CreateItem/CreateItem.tsx | 5 +- .../PickOrderDetail/ApprovalContent.tsx | 132 ++++ .../PickOrderDetail/ApprovalForm.tsx | 137 ++++ .../PickOrderDetail/PickOrderDetail.tsx | 666 ++++++++++++++++-- .../PickOrderDetailWrapper.tsx | 12 +- src/components/PickOrderDetail/QcContent.tsx | 247 +++++++ src/components/PickOrderDetail/QcForm.tsx | 119 ++++ .../ConsolidatedPickOrders.tsx | 39 +- src/components/PoDetail/PoDetail.tsx | 5 +- src/components/PoDetail/PoInputGrid.tsx | 7 +- src/components/PoDetail/QcForm.tsx | 2 +- 15 files changed, 1393 insertions(+), 100 deletions(-) create mode 100644 src/app/utils/gridUtil.tsx create mode 100644 src/components/PickOrderDetail/ApprovalContent.tsx create mode 100644 src/components/PickOrderDetail/ApprovalForm.tsx create mode 100644 src/components/PickOrderDetail/QcContent.tsx create mode 100644 src/components/PickOrderDetail/QcForm.tsx diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index faf2ff5..7980d51 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -6,14 +6,55 @@ import { cache } from "react"; import { serverFetchJson } from "@/app/utils/fetchUtil"; import { QcItemResult } from "../settings/qcItem"; import { RecordsRes } from "../utils"; -import { ConsoPickOrderResult, PickOrderLineWithSuggestedLot, PickOrderResult, PreReleasePickOrderSummary } from "."; +import { ConsoPickOrderResult, PickOrderLineWithSuggestedLot, PickOrderResult, PreReleasePickOrderSummary, StockOutLine } from "."; +import { PurchaseQcResult } from "../po/actions"; // import { BASE_API_URL } from "@/config/api"; +export interface PostStockOutLiineResponse { + id: number | null; + name: string; + code: string; + type?: string + message: string | null; + errorPosition: string | keyof T; + entity: T | T[] +} export interface ReleasePickOrderInputs { consoCode: string assignTo: number, } +export interface CreateStockOutLine { + consoCode: string, + pickOrderLineId: number, + inventoryLotLineId: number, + qty: number, +} + +export interface UpdateStockOutLine { + id: number, + // consoCode: String, + itemId: number, + qty: number, + pickOrderLineId: number, + inventoryLotLineId?: number, + status: string, + pickTime?: string, + // pickerId: number? +} + +export interface PickOrderQcInput { + qty: number + status: string + qcResult: PurchaseQcResult[]; +} + +export interface PickOrderApprovalInput { + allowQty: number + rejectQty: number + status: string +} + export const consolidatePickOrder = async (ids: number[]) => { const pickOrder = await serverFetchJson(`${BASE_API_URL}/pickOrder/conso`, { method: "POST", @@ -80,6 +121,13 @@ export const consolidatePickOrder_revert = async (ids: number[]) => { } }); + export const fetchStockOutLineClient = cache(async (pickOrderLineId: number) => { + return serverFetchJson(`${BASE_API_URL}/stockOutLine/getByPickOrderLineId/${pickOrderLineId}`, { + method: 'GET', + next: { tags: ["pickorder"] }, + }); + }); + export const fetchConsoDetail = cache(async (consoCode: string) => { return serverFetchJson(`${BASE_API_URL}/pickOrder/pre-release-info/${consoCode}`, { method: 'GET', @@ -98,4 +146,27 @@ export const consolidatePickOrder_revert = async (ids: number[]) => { }); revalidateTag("pickorder"); return po + } + + export const createStockOutLine = async (data: CreateStockOutLine) => { + console.log("triggering") + const po = await serverFetchJson>(`${BASE_API_URL}/stockOutLine/create`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + revalidateTag("pickorder"); + return po + } + + export const updateStockOutLine = async (data: UpdateStockOutLine) => { + console.log(data) + const po = await serverFetchJson> + (`${BASE_API_URL}/stockOutLine/update`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + revalidateTag("pickorder"); + return po } \ No newline at end of file diff --git a/src/app/api/qc/actions.ts b/src/app/api/qc/actions.ts index 66fce32..fb8e38b 100644 --- a/src/app/api/qc/actions.ts +++ b/src/app/api/qc/actions.ts @@ -8,7 +8,13 @@ import { serverFetchJson } from "../../utils/fetchUtil"; import { QcItemWithChecks } from "."; export interface QcResult { - + id: number + qcItemId: number + name: string + code: string + stockInLineId?: number + stockOutLineId?: number + failQty: number } export const fetchQcItemCheck = cache(async (itemId?: number) => { @@ -21,7 +27,13 @@ export const fetchQcItemCheck = cache(async (itemId?: number) => { export const fetchQcResult = cache(async (id: number) => { - return serverFetchJson(`${BASE_API_URL}/qcResult/${id}`, { + return serverFetchJson(`${BASE_API_URL}/qcResult/${id}`, { + next: { tags: ["qc"] }, + }); +}); + +export const fetchPickOrderQcResult = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/qcResult/pick-order/${id}`, { next: { tags: ["qc"] }, }); }); \ No newline at end of file diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 29f0c7c..78f9834 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -1,6 +1,7 @@ import dayjs, { ConfigType, Dayjs } from "dayjs"; import { Uom } from "../api/settings/uom"; import { ListIterateeCustom, every, isArray, isNaN, isNull, isUndefined, take } from "lodash"; +import { Box, BoxProps, } from "@mui/material"; export const manhourFormatter = new Intl.NumberFormat("en-HK", { minimumFractionDigits: 2, @@ -74,8 +75,11 @@ export const stockInLineStatusMap: { [status: string]: number } = { export const stockOutLineStatusMap: { [status: string]: number } = { "draft": 0, "pending": 1, // waiting for qc + "determine1": 2, // waiting for qc + "lot-change": 3, // waiting for qc // after qc = completed - "completed": 2, + "completed": 4, + "rejected": 5, }; export const pickOrderStatusMap: { [status: string]: number } = { @@ -92,4 +96,3 @@ export const calculateWeight = (qty: number, uom: Uom) => { export const returnWeightUnit = (uom: Uom) => { return uom.unit4 || uom.unit3 || uom.unit2 || uom.unit1; } - diff --git a/src/app/utils/gridUtil.tsx b/src/app/utils/gridUtil.tsx new file mode 100644 index 0000000..d8a1699 --- /dev/null +++ b/src/app/utils/gridUtil.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Box, Grid } from "@mui/material"; + +export function FitAllCell(list: (string | number)[]) { + return ( + + {list.map((item, index) => ( + + {`${item}`} + + ))} + + ); +} diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index 1a9babf..9142128 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -160,7 +160,9 @@ const CreateItem: React.FC = ({ component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - + {t(`${mode} ${title}`)} @@ -185,7 +187,6 @@ const CreateItem: React.FC = ({ variant="contained" startIcon={} type="submit" - // disabled={submitDisabled} > {isEditMode ? t("Save") : t("Confirm")} diff --git a/src/components/PickOrderDetail/ApprovalContent.tsx b/src/components/PickOrderDetail/ApprovalContent.tsx new file mode 100644 index 0000000..18a5e9f --- /dev/null +++ b/src/components/PickOrderDetail/ApprovalContent.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { + Box, + Card, + CardContent, + Grid, + Stack, + TextField, + Tooltip, + Typography, +} from "@mui/material"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid"; +import { useCallback, useEffect, useMemo } from "react"; +import { + GridColDef, + GridRowIdGetter, + GridRowModel, + useGridApiContext, + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiRef, +} from "@mui/x-data-grid"; +import InputDataGrid from "../InputDataGrid"; +import { TableRow } from "../InputDataGrid/InputDataGrid"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { GridEditInputCell } from "@mui/x-data-grid"; +import { StockInLine } from "@/app/api/po"; +import { stockInLineStatusMap } from "@/app/utils/formatUtil"; +import { PickOrderApprovalInput } from "@/app/api/pickorder/actions"; +import { StockOutLine } from "@/app/api/pickorder"; + +interface Props { +// approvalDefaultValues: StockInLine; +// qc: QcItemWithChecks[]; + approvalDefaultValues: StockOutLine & PickOrderApprovalInput; + + disabled: boolean +} + +const ApprovalContent: React.FC = ({ +// qc, + approvalDefaultValues, + disabled + }) => { + const { t } = useTranslation("purchaseOrder"); + const apiRef = useGridApiRef(); + const { + register, + formState: { errors, defaultValues, touchedFields }, + watch, + control, + setValue, + getValues, + reset, + resetField, + setError, + clearErrors, + } = useFormContext(); + console.log(approvalDefaultValues) + +// const status = "rejected" + const totalQty = approvalDefaultValues.qty + const allowQty = watch("allowQty"); + const rejectQty = watch("rejectQty"); + + return ( + + + + {t(`Lot Change Approval`)} + + + {/* + + {t(`to be processed`)}: {approvalDefaultValues.rejectQty - rejectQty} + + */} + + + + + + + + + + + + ); +}; +export default ApprovalContent; diff --git a/src/components/PickOrderDetail/ApprovalForm.tsx b/src/components/PickOrderDetail/ApprovalForm.tsx new file mode 100644 index 0000000..5eabaea --- /dev/null +++ b/src/components/PickOrderDetail/ApprovalForm.tsx @@ -0,0 +1,137 @@ +import { QcItemWithChecks } from "@/app/api/qc"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import { + PickOrderApprovalInput, + PickOrderQcInput, + updateStockOutLine, + UpdateStockOutLine, +} from "@/app/api/pickorder/actions"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import QcContent from "./QcContent"; +import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Check } from "@mui/icons-material"; +import { StockOutLine } from "@/app/api/pickorder"; +import dayjs from "dayjs"; +import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; +import ApprovalContent from "./ApprovalContent"; + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + // overflow: "scroll", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "60%", sm: "60%", md: "60%" }, +}; + +interface Props extends Omit { + // qc: QcItemWithChecks[]; + approvalDefaultValues: StockOutLine & PickOrderApprovalInput; + disabled: boolean; +} + +const ApprovalForm: React.FC = ({ + // qc, + approvalDefaultValues, + disabled, + open, + onClose, +}) => { + const { setIsUploading } = useUploadContext(); + const { t } = useTranslation("pickOrder"); + const formProps = useForm({ + defaultValues: { + allowQty: approvalDefaultValues.qty, + rejectQty: 0, + status: approvalDefaultValues.status, + }, + }); + + const errors = formProps.formState.errors; + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + // reset(); + }, + [onClose] + ); + + const onSubmit = useCallback>( + async (data, event) => { + console.log(data); + // checking later + // post + let hasError = false; + if (data.allowQty + data.rejectQty != approvalDefaultValues.qty) { + formProps.setError("allowQty", { + message: "illegal qty", + type: "required", + }); + formProps.setError("rejectQty", { + message: "illegal qty", + type: "required", + }); + } + if (hasError) { + return + } + const postData: UpdateStockOutLine = { + id: approvalDefaultValues.id, + itemId: approvalDefaultValues.itemId, + pickOrderLineId: approvalDefaultValues.pickOrderLineId, + qty: data.allowQty, //allow qty + status: data.status, + // pickTime: dayjs().format(`${INPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`), + }; + console.log(postData); + // return; + const res = await updateStockOutLine(postData); + if (res) { + console.log(res); + closeHandler({}, "backdropClick"); + } else { + console.log(res); + console.log("bug la"); + } + }, + [t] + ); + return ( + <> + + + + + + {!disabled ? ( + + ) : undefined} + + + + + + ); +}; +export default ApprovalForm; diff --git a/src/components/PickOrderDetail/PickOrderDetail.tsx b/src/components/PickOrderDetail/PickOrderDetail.tsx index fa077b5..8ae23d3 100644 --- a/src/components/PickOrderDetail/PickOrderDetail.tsx +++ b/src/components/PickOrderDetail/PickOrderDetail.tsx @@ -1,6 +1,7 @@ "use client"; import { + Box, Button, ButtonProps, Card, @@ -9,25 +10,61 @@ import { CircularProgress, Grid, Stack, + Tooltip, Typography, } from "@mui/material"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { useTranslation } from "react-i18next"; import StyledDataGrid from "../StyledDataGrid"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { GridColDef } from "@mui/x-data-grid"; +import { + GridColDef, + GridRowId, + GridRowIdGetter, + GridRowModel, + GridRowModes, + useGridApiRef, + GridRenderEditCellParams, + GridEditInputCell, + GridRowParams, +} from "@mui/x-data-grid"; import { PlayArrow } from "@mui/icons-material"; import DoneIcon from "@mui/icons-material/Done"; import { GridRowSelectionModel } from "@mui/x-data-grid"; import { useQcCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; -import { fetchPickOrderLineClient } from "@/app/api/pickorder/actions"; -import { PickOrderLineWithSuggestedLot } from "@/app/api/pickorder"; +import { + CreateStockOutLine, + createStockOutLine, + fetchPickOrderLineClient, + fetchStockOutLineClient, + PickOrderApprovalInput, + PickOrderQcInput, +} from "@/app/api/pickorder/actions"; +import { + PickOrderLineWithSuggestedLot, + StockOutLine, +} from "@/app/api/pickorder"; import { Pageable } from "@/app/utils/fetchUtil"; import { QrCodeInfo } from "@/app/api/qrcode"; import { QrCode } from "../QrCode"; import { fetchLotDetail, LotLineInfo } from "@/app/api/inventory/actions"; import { GridRowModesModel } from "@mui/x-data-grid"; +import { stockOutLineStatusMap } from "@/app/utils/formatUtil"; +import { GridActionsCellItem } from "@mui/x-data-grid"; +import DoDisturbIcon from "@mui/icons-material/DoDisturb"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import { FitAllCell } from "@/app/utils/gridUtil"; +import { QcItemWithChecks } from "@/app/api/qc"; +import QcForm from "./QcForm"; +import { fetchPickOrderQcResult, QcResult } from "@/app/api/qc/actions"; +import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; +import AutoFixNormalIcon from "@mui/icons-material/AutoFixNormal"; +import ApprovalForm from "./ApprovalForm"; +import InfoIcon from "@mui/icons-material/Info"; +import VerifiedIcon from "@mui/icons-material/Verified"; interface Props { + qc: QcItemWithChecks[]; consoCode: string; } interface IsLoadingModel { @@ -35,9 +72,50 @@ interface IsLoadingModel { stockOutLineTable: boolean; } -const PickOrderDetail: React.FC = ({ consoCode }) => { +export type StockOutLineEntryError = { + [field in keyof StockOutLine]?: string; +}; + +export type StockOutLineRow = Partial< + StockOutLine & { + id: number; + isActive: boolean | undefined; + _isNew: boolean; + _error: StockOutLineEntryError; + } +>; + +class ProcessRowUpdateError extends Error { + public readonly row: StockOutLineRow; + public readonly errors: StockOutLineEntryError | undefined; + constructor( + row: StockOutLineRow, + message?: string, + errors?: StockOutLineEntryError + ) { + super(message); + this.row = row; + this.errors = errors; + + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} + +export type formDefaultValues = StockOutLine & + (PickOrderQcInput | PickOrderApprovalInput); + +const PickOrderDetail: React.FC = ({ consoCode, qc }) => { const { t } = useTranslation("pickOrder"); - const [selectedRow, setSelectRow] = useState(); + const apiRef = useGridApiRef(); + const [qcResult, setQcResult] = useState([] as QcResult[]); + const [selectedRow, setSelectedRow] = useState([]); + const [currPol, setCurrPol] = useState(); + const [isChangeLotSolId, setIsChangeLotSolId] = useState( + undefined + ); + + const [formDefaultValues, setFormDefaultValues] = + useState(); const [isLoadingModel, setIsLoadingModel] = useState({ pickOrderLineTable: false, stockOutLineTable: false, @@ -53,6 +131,8 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { const [polTotalCount, setPolTotalCount] = useState(0); const [solTotalCount, setSolTotalCount] = useState(0); const [rowModesModel, setRowModesModel] = useState({}); + const [btnIsLoading, setBtnIsLoading] = useState(false); + const { setIsUploading } = useUploadContext(); const [pickOrderLine, setPickOrderLine] = useState< PickOrderLineWithSuggestedLot[] @@ -80,58 +160,36 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { headerName: "uom", flex: 1, }, + { + field: "lotLineId", + headerName: "lotLineId", + flex: 1, + }, { field: "warehouse", headerName: "location", flex: 1, + renderCell: (params) => { + if (!params.row.warehouse) return <>; + const warehouseList = JSON.parse(params.row.warehouse) as string[]; + return FitAllCell(warehouseList); + }, }, { field: "suggestedLotNo", headerName: "suggestedLotNo", flex: 1.2, + renderCell: (params) => { + if (!params.row.suggestedLotNo) return <>; + const suggestedLotNoList = JSON.parse( + params.row.suggestedLotNo + ) as string[]; + return FitAllCell(suggestedLotNoList); + }, }, ], [] ); - const [stockOutLine, setStockOutLine] = useState([]); - const stockOutLineColumns = useMemo( - () => [ - { - field: "code", - headerName: "actual lot (out line", - flex: 1, - }, - ], - [] - ); - - const handleStartPickOrder = useCallback(async () => {}, []); - - const handleCompletePickOrder = useCallback(async () => {}, []); - - useEffect(() => { - console.log(selectedRow); - }, [selectedRow]); - - const buttonData = useMemo( - () => ({ - buttonName: "complete", - title: t("Do you want to complete?"), - confirmButtonText: t("Complete"), - successTitle: t("Complete Success"), - errorTitle: t("Complete Fail"), - buttonText: t("Complete PO"), - buttonIcon: , - buttonColor: "info", - disabled: true, - }), - [] - ); - - const [isOpenScanner, setOpenScanner] = useState(false); - const onOpenScanner = useCallback(() => { - setOpenScanner((prev) => !prev); - }, []); const fetchPickOrderLine = useCallback( async (params: Record) => { @@ -158,27 +216,397 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { }, [fetchPickOrderLineClient, consoCode] ); + + const buttonData = useMemo( + () => ({ + buttonName: "complete", + title: t("Do you want to complete?"), + confirmButtonText: t("Complete"), + successTitle: t("Complete Success"), + errorTitle: t("Complete Fail"), + buttonText: t("Complete PO"), + buttonIcon: , + buttonColor: "info", + disabled: true, + }), + [] + ); + + const [stockOutLine, setStockOutLine] = useState([]); + + const getRowId = useCallback>( + (row) => row.id as number, + [] + ); + const [qcOpen, setQcOpen] = useState(false); + const [approvalOpen, setApprovalOpen] = useState(false); + + const closeQcModal = useCallback(() => { + setQcOpen(false); + }, []); + const openQcModal = useCallback(() => { + setQcOpen(true); + }, []); + + const closeApprovalModal = useCallback(() => { + setApprovalOpen(false); + }, []); + const openApprovalModal = useCallback(() => { + setApprovalOpen(true); + }, []); + + const triggerRefetch = useCallback(() => { + setSelectedRow((prev) => prev); + }, []); + + const handleDelete = useCallback( + (id: GridRowId) => () => { + setStockOutLine((prev) => prev.filter((e) => getRowId(e) !== id)); + }, + [getRowId] + ); + + const handleStart = useCallback( + (id: GridRowId, params: any) => () => { + setBtnIsLoading(true); + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setTimeout(async () => { + // post stock in line + const oldId = params.row.id; + console.log(params.row); + // console.log(currPol); + const postData = { + consoCode: consoCode, + pickOrderLineId: params.row.pickOrderLineId, + inventoryLotLineId: params.row.inventoryLotLineId, + qty: params.row.qty, + } as CreateStockOutLine; + console.log(postData); + // return + console.log("triggering"); + const res = await createStockOutLine(postData); + if (res) { + console.log(res); + setStockOutLine((prev) => + prev.map((p) => (p.id === oldId ? (res.entity as StockOutLine) : p)) + ); + } + setBtnIsLoading(false); + // do post directly to test + // openStartModal(); + }, 500); + }, + [createStockOutLine] + ); + + useEffect(() => { + console.log(stockOutLine); + }, [stockOutLine]); + + const handleApproval = useCallback( + (id: GridRowId, params: any) => async () => { + setBtnIsLoading(true); + console.log(params.row.qty); + console.log(params.row); + setFormDefaultValues({ + ...(params.row as StockOutLine), + status: "lot-change", + } as StockOutLine & PickOrderApprovalInput); + setTimeout(() => { + // open qc modal + console.log("delayed"); + openApprovalModal(); + setBtnIsLoading(false); + }, 200); + }, + [] + ); + + const handleLotChange = useCallback( + (id: GridRowId, params: GridRowParams) => async () => { + setOpenScanner((prev) => !prev); + console.log(id); + setIsChangeLotSolId((prev) => { + if (prev != undefined) return undefined + return id as number + }); + }, + [] + ); + + useEffect(() => { + console.log(isChangeLotSolId) + }, [isChangeLotSolId]) + + const handleComplete = useCallback( + (id: GridRowId, params: any) => async () => { + setBtnIsLoading(true); + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + getQcResult(id as number).then((qcResult) => { + setQcResult(qcResult); + }); + console.log(params.row.qty); + console.log(params.row); + setFormDefaultValues({ + ...(params.row as StockOutLine), + qty: params.row.qty, + status: "completed", + } as StockOutLine & PickOrderQcInput); + setTimeout(() => { + // open qc modal + console.log("delayed"); + openQcModal(); + setBtnIsLoading(false); + }, 200); + }, + [] + ); + + const stockOutLineColumns = useMemo( + () => [ + { + field: "itemName", + headerName: "item name", + flex: 1, + }, + { + field: "qty", + headerName: "qty", + editable: true, + flex: 1, + type: "number", + // renderEditCell(params: GridRenderEditCellParams) { + // const errorMessage = + // params.row._error?.[params.field as keyof StockOutLineEntryError]; + // const content = ( + // + // ); + // return errorMessage ? ( + // + // {content} + // + // ) : ( + // content + // ); + // }, + }, + { + field: "lotNo", + headerName: "lotNo", + flex: 1, + }, + { + field: "status", + headerName: t("status"), + flex: 0.5, + renderCell: (params) => { + return t(`${params.row.status}`); + }, + }, + { + field: "actions", + type: "actions", + headerName: `${t("start")} + | ${t("approval")} + | ${t("lot change")} + | ${t("checkout")} + | ${t("delete")}`, + flex: 1.5, + cellClassName: "actions", + getActions: (params) => { + const status = params.row.status.toLowerCase(); + return [ + } + label="start" + sx={{ + color: "primary.main", + // marginRight: 1, + }} + disabled={!(stockOutLineStatusMap[status] === 0)} + // set _isNew to false after posting + // or check status + onClick={handleStart(params.row.id, params)} + color="inherit" + key="edit" + />, + } + label="approval" + sx={{ + color: "primary.main", + // marginRight: 1, + }} + disabled={stockOutLineStatusMap[status] !== 2} + // set _isNew to false after posting + // or check status + onClick={handleApproval(params.row.id, params)} // start scanning for that row + color="inherit" + key="edit" + />, + } + label="lot change" /// + sx={{ + color: "primary.main", + // marginRight: 1, + }} + disabled={stockOutLineStatusMap[status] !== 3} + // set _isNew to false after posting + // or check status + onClick={handleLotChange(params.row.id, params)} // start scanning for that row + color="inherit" + key="edit" + />, + } + label="qcAndPick" + sx={{ + color: "primary.main", + // marginRight: 1, + }} + disabled={ + !params.row.inventoryLotLineId || + stockOutLineStatusMap[status] === 2 || + stockOutLineStatusMap[status] === 5 + } + // set _isNew to false after posting + // or check status + onClick={handleComplete(params.row.id, params)} + color="inherit" + key="edit" + />, + } + label="Delete" + sx={{ + color: "error.main", + }} + disabled={stockOutLineStatusMap[status] > 0} + onClick={handleDelete(params.row.id)} + />, + } + label="debug button" + sx={{ + color: "error.main", + }} + onClick={() => console.log(params.row)} + />, + ]; + }, + }, + ], + [stockOutLineStatusMap, handleStart, handleDelete] + ); + const fetchStockOutLine = useCallback( - async (params: Record) => {}, + async (params: Record, selectedRow: GridRowSelectionModel) => { + const _selectedRow = selectedRow as number[]; + console.log(params); + console.log(_selectedRow); + // fetch + const res = await fetchStockOutLineClient(_selectedRow[0]); + console.log(res); + // set state + setStockOutLine(res); + }, [] ); + const addRow = useCallback( + (qrcode: LotLineInfo) => { + const newEntry = { + id: Date.now(), + _isNew: true, + itemId: qrcode.itemId, + itemName: qrcode.itemName, + itemNo: qrcode.itemNo, + lotNo: qrcode.lotNo, + inventoryLotLineId: qrcode.inventoryLotLineId, + qty: 0, + pickOrderLineId: selectedRow[0] as number, + status: "draft", + }; + setStockOutLine((prev) => [...prev, newEntry]); + setRowModesModel((model) => ({ + ...model, + [getRowId(newEntry)]: { + mode: GridRowModes.Edit, + }, + })); + }, + [getRowId, selectedRow] + ); + + // need modify this later + const changeRow = useCallback( + (id: number, qrcode: LotLineInfo) => { + console.log(stockOutLine); + console.log(stockOutLine.find((line) => line.id === id)); + const rowToSave = { + ...stockOutLine.find((line) => line.id === id), + itemId: qrcode.itemId, + itemName: qrcode.itemName, + itemNo: qrcode.itemNo, + lotNo: qrcode.lotNo, + inventoryLotLineId: qrcode.inventoryLotLineId, + }; + console.log(rowToSave); + const newEntries = stockOutLine.map((e) => + getRowId(e) === id ? rowToSave : e + ); + setStockOutLine(newEntries as StockOutLine[]); + }, + [stockOutLine, getRowId] + ); + useEffect(() => { fetchPickOrderLine(polCriteriaArgs); }, [polCriteriaArgs]); useEffect(() => { - fetchStockOutLine(solCriteriaArgs); - }, [solCriteriaArgs]); + if (!qcOpen || !approvalOpen) { + console.log("triggering") + triggerRefetch(); + } + if (selectedRow.length > 0) fetchStockOutLine(solCriteriaArgs, selectedRow); + }, [qcOpen, approvalOpen, solCriteriaArgs, selectedRow, triggerRefetch]); const getLotDetail = useCallback( async (stockInLineId: number): Promise => { const res = await fetchLotDetail(stockInLineId); + console.log("res"); + console.log(res); return res; }, [fetchLotDetail] ); + const getQcResult = useCallback( + async (stockOutLineId: number): Promise => { + const res = await fetchPickOrderQcResult(stockOutLineId); + console.log("res"); + console.log(res); + return res; + }, + [fetchPickOrderQcResult] + ); + + const [isOpenScanner, setOpenScanner] = useState(false); + const onOpenScanner = useCallback(() => { + setOpenScanner((prev) => !prev); + }, []); + const scanner = useQcCodeScanner(); useEffect(() => { if (isOpenScanner && !scanner.isScanning) { @@ -188,24 +616,102 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { } }, [isOpenScanner]); + const homemade_Qrcode = { + stockInLineId: 156, // eggs + // stockInLineId: 162, // chicken wings + }; + useEffect(() => { if (scanner.values.length > 0) { console.log(scanner.values[0]); const data: QrCodeInfo = JSON.parse(scanner.values[0]); console.log(data); if (data.stockInLineId) { - console.log("still got in"); - console.log(data.stockInLineId); + setIsUploading(true); // fetch - getLotDetail(data.stockInLineId).then((value) => {}); + getLotDetail(data.stockInLineId).then((qrcode) => { + // add row + if (isChangeLotSolId) { + changeRow(isChangeLotSolId, qrcode); + } else { + addRow(qrcode); + } + }); + setIsUploading(false); } scanner.resetScan(); } - }, [scanner.values]); + }, [ + isChangeLotSolId, + scanner.values, + selectedRow, + changeRow, + addRow, + getLotDetail, + ]); - const homemade_Qrcode = { - stockInLineId: 156, - }; + const mannuallyAddRow = useCallback(() => { + getLotDetail(homemade_Qrcode.stockInLineId).then((qrcode) => { + addRow(qrcode); + // scanner.resetScan(); + }); + }, [addRow, homemade_Qrcode]); + + const validation = useCallback( + ( + newRow: GridRowModel + // rowModel: GridRowSelectionModel + ): StockOutLineEntryError | undefined => { + const error: StockOutLineEntryError = {}; + const checkQty = currPol?.qty; + console.log(newRow); + if (!newRow.qty || newRow.qty <= 0) { + error["qty"] = t("illegal qty"); + } + return Object.keys(error).length > 0 ? error : undefined; + }, + [currPol] + ); + + const processRowUpdate = useCallback( + ( + newRow: GridRowModel, + originalRow: GridRowModel + ) => { + const errors = validation(newRow); // change to validation + console.log(newRow); + if (errors) { + throw new ProcessRowUpdateError( + originalRow, + "validation error", + errors + ); + } + const { _isNew, _error, ...updatedRow } = newRow; + const rowToSave = { + ...updatedRow, + } satisfies StockOutLineRow; + console.log(rowToSave); + const newEntries = stockOutLine.map((e) => + getRowId(e) === getRowId(originalRow) ? rowToSave : e + ); + console.log(newEntries); + + setStockOutLine(newEntries as StockOutLine[]); + return rowToSave; + }, + [stockOutLine, validation] + ); + + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const oldRow = updateError.row; + + apiRef.current.updateRows([{ ...oldRow, _error: errors }]); + }, + [apiRef] + ); return ( <> @@ -233,7 +739,13 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { justifyContent="end" alignItems="end" > - + @@ -254,7 +766,7 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { */} - {isLoadingModel.pickOrderLineTable ? ( + {isLoadingModel.pickOrderLineTable && pickOrderLine == undefined ? ( ) : ( = ({ consoCode }) => { columns={pickOrderLineColumns} rowSelectionModel={selectedRow} onRowSelectionModelChange={(newRowSelectionModel) => { - setSelectRow(newRowSelectionModel); + setSelectedRow(newRowSelectionModel); + if (newRowSelectionModel && newRowSelectionModel.length > 0) { + const pol = pickOrderLine.find( + (item) => item.id === newRowSelectionModel[0] + ); + console.log(pol); + setCurrPol(pol); + } }} initialState={{ pagination: { @@ -282,15 +801,22 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { { + const status = params.row.status.toLowerCase(); + return ( + stockOutLineStatusMap[status] === 0 || + stockOutLineStatusMap[status] === 3 + ); + }} initialState={{ pagination: { paginationModel: { pageSize: 10, page: 0 }, @@ -308,6 +834,26 @@ const PickOrderDetail: React.FC = ({ consoCode }) => { + {/* modals */} + {qcOpen && formDefaultValues != undefined && ( + + )} + {approvalOpen && formDefaultValues != undefined && ( + + )} ); }; diff --git a/src/components/PickOrderDetail/PickOrderDetailWrapper.tsx b/src/components/PickOrderDetail/PickOrderDetailWrapper.tsx index f201af7..a4bc31a 100644 --- a/src/components/PickOrderDetail/PickOrderDetailWrapper.tsx +++ b/src/components/PickOrderDetail/PickOrderDetailWrapper.tsx @@ -20,14 +20,14 @@ type Props = { }; const PoDetailWrapper: React.FC & SubComponents = async ({ consoCode }) => { -// const [poWithStockInLine, warehouse, qc] = await Promise.all([ -// fetchPoWithStockInLines(id), -// fetchWarehouseList(), -// fetchQcItemCheck(), -// ]); + const [ + qc + ] = await Promise.all([ + fetchQcItemCheck(), + ]); // const poWithStockInLine = await fetchPoWithStockInLines(id) - return ; + return ; }; PoDetailWrapper.Loading = PickOrderDetailLoading; diff --git a/src/components/PickOrderDetail/QcContent.tsx b/src/components/PickOrderDetail/QcContent.tsx new file mode 100644 index 0000000..7c24a1f --- /dev/null +++ b/src/components/PickOrderDetail/QcContent.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; +import { + Box, + Card, + CardContent, + Grid, + Stack, + TextField, + Tooltip, + Typography, +} from "@mui/material"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { + GridColDef, + GridRowIdGetter, + GridRowModel, + useGridApiContext, + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiRef, +} from "@mui/x-data-grid"; +import InputDataGrid from "../InputDataGrid"; +import { TableRow } from "../InputDataGrid/InputDataGrid"; +import { GridEditInputCell } from "@mui/x-data-grid"; +import { StockInLine } from "@/app/api/po"; +import { stockInLineStatusMap } from "@/app/utils/formatUtil"; +import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; +import { QcItemWithChecks } from "@/app/api/qc"; +import axios from "@/app/(main)/axios/axiosInstance"; +import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import axiosInstance from "@/app/(main)/axios/axiosInstance"; +import TwoLineCell from "../PoDetail/TwoLineCell"; +import QcSelect from "../PoDetail/QcSelect"; +import { PickOrderQcInput } from "@/app/api/pickorder/actions"; + +interface Props { + qcDefaultValues: PickOrderQcInput + qc: QcItemWithChecks[]; + disabled: boolean +} +type EntryError = + | { + [field in keyof PurchaseQcResult]?: string; + } + | undefined; + +type PoQcRow = TableRow, EntryError>; +// fetchQcItemCheck +const QcContent: React.FC = ({ qc, qcDefaultValues, disabled }) => { + const { t } = useTranslation("purchaseOrder"); + const apiRef = useGridApiRef(); + const { + register, + formState: { errors, defaultValues, touchedFields }, + watch, + control, + setValue, + getValues, + reset, + resetField, + setError, + clearErrors, + } = useFormContext(); + console.log(qcDefaultValues); + console.log(defaultValues); + + //// validate form + const accQty = watch("qty"); + const validateForm = useCallback(() => { + console.log(accQty); + if (accQty > qcDefaultValues.qty) { + setError("qty", { + message: `${t("qty must not greater than")} ${qcDefaultValues.qty}`, + type: "required", + }); + } + if (accQty < 1) { + setError("qty", { + message: t("minimal value is 1"), + type: "required", + }); + } + if (isNaN(accQty)) { + setError("qty", { + message: t("value must be a number"), + type: "required", + }); + } + }, [accQty]); + + useEffect(() => { + clearErrors(); + validateForm(); + }, [validateForm]); + // const [recordQty, setRecordQty] = useState(0); + const columns = useMemo( + () => [ + { + field: "qcItemId", + headerName: t("qc Check"), + flex: 1, + editable: !disabled, + valueFormatter(params) { + const row = params.id ? params.api.getRow(params.id) : null; + if (!row) { + return null; + } + const Qc = qc.find((q) => q.id === row.qcItemId); + return Qc ? `${Qc.code} - ${Qc.name}` : t("Please select QC"); + }, + renderCell(params: GridRenderCellParams) { + console.log(params.value); + return {params.formattedValue}; + }, + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = + params.row._error?.[params.field as keyof PurchaseQcResult]; + console.log(errorMessage); + const content = ( + { + await params.api.setEditCellValue({ + id: params.id, + field: "qcItemId", + value: qcItemId, + }); + // await params.api.setEditCellValue({ + // id: params.id, + // field: "type", + // value: "determine1", + // }); + }} + /> + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + }, + }, + { + field: "failQty", + headerName: t("failQty"), + flex: 1, + editable: !disabled, + type: "number", + renderEditCell(params: GridRenderEditCellParams) { + // const recordQty = params.row.qty + // if (recordQty !== undefined) { + // setUnrecordQty((prev) => prev - recordQty) + // } + const errorMessage = + params.row._error?.[params.field as keyof PurchaseQcResult]; + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + content + ); + }, + }, + ], + [qc] + ); + /// validate datagrid + const validation = useCallback( + (newRow: GridRowModel): EntryError => { + const error: EntryError = {}; + const { qcItemId, failQty } = newRow; + if (!qcItemId || qcItemId <= 0) { + error["qcItemId"] = t("select qc"); + } + if (!failQty || failQty <= 0) { + error["failQty"] = t("enter a failQty"); + } + if (failQty && failQty > qcDefaultValues.qty) { + error["failQty"] = t("qty too big"); + } + return Object.keys(error).length > 0 ? error : undefined; + }, + [] + ); + + return ( + + + + {t("Qc Detail")} + + + + + + + + + + + apiRef={apiRef} + checkboxSelection={false} + _formKey={"qcResult"} + columns={columns} + validateRow={validation} + needAdd={!disabled} + /> + + + + ); +}; +export default QcContent; diff --git a/src/components/PickOrderDetail/QcForm.tsx b/src/components/PickOrderDetail/QcForm.tsx new file mode 100644 index 0000000..d3b53ad --- /dev/null +++ b/src/components/PickOrderDetail/QcForm.tsx @@ -0,0 +1,119 @@ +import { QcItemWithChecks } from "@/app/api/qc"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import { PickOrderQcInput, updateStockOutLine, UpdateStockOutLine } from "@/app/api/pickorder/actions"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import QcContent from "./QcContent"; +import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Check } from "@mui/icons-material"; +import { StockOutLine } from "@/app/api/pickorder"; +import dayjs from "dayjs"; +import { INPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + // overflow: "scroll", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "60%", sm: "60%", md: "60%" }, +}; + +interface Props extends Omit { + qc: QcItemWithChecks[]; + qcDefaultValues: StockOutLine & PickOrderQcInput; + disabled: boolean; +} + +const QcForm: React.FC = ({ + qc, + qcDefaultValues, + disabled, + open, + onClose, +}) => { + const { setIsUploading } = useUploadContext(); + const { t } = useTranslation("pickOrder"); + const formProps = useForm({ + defaultValues: { + qty: qcDefaultValues.qty, + status: qcDefaultValues.status + }, + }); + + const errors = formProps.formState.errors; + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + // reset(); + }, + [onClose] + ); + + const onSubmit = useCallback>( + async (data, event) => { + console.log(data); + console.log(qcDefaultValues); + // checking later + // post + const postData: UpdateStockOutLine = { + id: qcDefaultValues.id, + itemId: qcDefaultValues.itemId, + pickOrderLineId: qcDefaultValues.pickOrderLineId, + inventoryLotLineId: qcDefaultValues.inventoryLotLineId, + qty: data.qty, + status: data.status, + // pickTime: dayjs().format(`${INPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`), + } + console.log(postData) + // return + const res = await updateStockOutLine(postData) + if (res) { + console.log(res) + closeHandler({}, "backdropClick"); + } else { + console.log(res) + console.log("bug la") + } + }, + [updateStockOutLine] + ); + return ( + <> + + + + + + {!disabled ? ( + + ) : undefined} + + + + + + ); +}; +export default QcForm; diff --git a/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx b/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx index 304c9fc..1dfa165 100644 --- a/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx +++ b/src/components/PickOrderSearch/ConsolidatedPickOrders.tsx @@ -109,8 +109,8 @@ const ConsolidatedPickOrders: React.FC = ({ filterArgs }) => { const onDetailClick = useCallback( (pickOrder: any) => { console.log(pickOrder); - const status = pickOrder.status - if (pickOrderStatusMap[status] >= 2) { + const status = pickOrder.status; + if (pickOrderStatusMap[status] >= 3) { router.push(`/pickorder/detail?consoCode=${pickOrder.consoCode}`); } else { openDetailModal(pickOrder.consoCode); @@ -210,31 +210,28 @@ const ConsolidatedPickOrders: React.FC = ({ filterArgs }) => { [closeDetailModal] ); - - const onChange = useCallback( - ( - event: React.SyntheticEvent, - newValue: NameList - ) => { - console.log(newValue); - formProps.setValue("assignTo", newValue.id); - }, - [] - ); + const onChange = useCallback( + (event: React.SyntheticEvent, newValue: NameList) => { + console.log(newValue); + formProps.setValue("assignTo", newValue.id); + }, + [] + ); const onSubmit = useCallback>( async (data, event) => { console.log(data); try { - const res = await releasePickOrder(data) - console.log(res) - if (res.status = 200) { - router.push(`/pickorder/detail?consoCode=${data.consoCode}`); - } else { - throw Error("hv error") + const res = await releasePickOrder(data); + console.log(res); + console.log(res.status); + if ((res.status === 200)) { + router.push(`/pickorder/detail?consoCode=${data.consoCode}`); + } else { + console.log(res); } } catch (error) { - console.log(error) + console.log(error); } }, [releasePickOrder] @@ -251,7 +248,7 @@ const ConsolidatedPickOrders: React.FC = ({ filterArgs }) => { useEffect(() => { if (consoCode) { fetchConso(consoCode); - formProps.setValue("consoCode", consoCode) + formProps.setValue("consoCode", consoCode); } }, [consoCode]); diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index a45f0e3..4c4ce19 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -75,6 +75,7 @@ import ReactQrCodeScannerModal, { import QrModal from "./QrModal"; import { PlayArrow } from "@mui/icons-material"; import DoneIcon from "@mui/icons-material/Done"; +import { QrCode } from "../QrCode"; type Props = { po: PoResult; @@ -272,6 +273,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { // break; } }, [purchaseOrder, handleStartPo, handleCompletePo]); + return ( <> = ({ po, qc, warehouse }) => { {/* {purchaseOrder.code} - {currPoStatus} */} - {purchaseOrder.code} - {t(`${purchaseOrder.status.toLowerCase()}`)} + {purchaseOrder.code} -{" "} + {t(`${purchaseOrder.status.toLowerCase()}`)} diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index ed1d770..77c4944 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -413,7 +413,6 @@ function PoInputGrid({ renderCell: (params) => { return t(`${params.row.status}`) } - // editable: true, }, { field: "actions", @@ -423,10 +422,10 @@ function PoInputGrid({ flex: 1.5, cellClassName: "actions", getActions: (params) => { - console.log(params.row.status); + // console.log(params.row.status); const status = params.row.status.toLowerCase(); - console.log(stockInLineStatusMap[status]); - console.log(session?.user?.abilities?.includes("APPROVAL")); + // console.log(stockInLineStatusMap[status]); + // console.log(session?.user?.abilities?.includes("APPROVAL")); return [ } diff --git a/src/components/PoDetail/QcForm.tsx b/src/components/PoDetail/QcForm.tsx index 7ed1040..4184745 100644 --- a/src/components/PoDetail/QcForm.tsx +++ b/src/components/PoDetail/QcForm.tsx @@ -96,7 +96,7 @@ const QcForm: React.FC = ({ qc, itemDetail, disabled }) => { clearErrors(); validateForm(); }, [validateForm]); - // const [recordQty, setRecordQty] = useState(0); + const columns = useMemo( () => [ {