| @@ -5,8 +5,9 @@ import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| //import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { QcItemWithChecks } from "."; | |||
| import { QcCategory, QcItemWithChecks } from "."; | |||
| // DEPRECIATED | |||
| export interface QcResult { | |||
| id: number; | |||
| qcItemId: number; | |||
| @@ -43,6 +44,16 @@ export const fetchQcItemCheck = cache(async (itemId?: number) => { | |||
| }); | |||
| }); | |||
| export const fetchQcCategory = cache(async (itemId: number, type: string = "IQC") => { | |||
| const params = new URLSearchParams({ | |||
| itemId: itemId.toString(), | |||
| type, | |||
| }).toString(); | |||
| return serverFetchJson<QcCategory>(`${BASE_API_URL}/qcCategories/items?${params}`, { | |||
| next: { tags: ["qc"] }, | |||
| }); | |||
| }); | |||
| export const fetchQcResult = cache(async (id: number) => { | |||
| return serverFetchJson<QcResult[]>(`${BASE_API_URL}/qcResult/${id}`, { | |||
| next: { tags: ["qc"] }, | |||
| @@ -15,23 +15,50 @@ export interface QcItemWithChecks { | |||
| description: string | undefined; | |||
| } | |||
| export interface QcResult{ | |||
| export interface QcCategory { | |||
| id: number, | |||
| code?: string, | |||
| name?: string, | |||
| description?: string, | |||
| qcItems: QcData[], | |||
| } | |||
| export interface QcData { | |||
| id?: number, | |||
| qcItemId: number, | |||
| code?: string, | |||
| name?: string, | |||
| order?: number, | |||
| description?: string, | |||
| // qcPassed: boolean | undefined | |||
| // failQty: number | undefined | |||
| // remarks: string | undefined | |||
| } | |||
| export interface QcResult extends QcData{ | |||
| id?: number; | |||
| qcItemId: number; | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| escalationLogId?: number; | |||
| stockInLineId?: number; | |||
| stockOutLineId?: number; | |||
| } | |||
| export interface QcData { | |||
| id?: number, | |||
| qcItemId: number, | |||
| code: string, | |||
| name: string, | |||
| description: string, | |||
| qcPassed: boolean | undefined | |||
| failQty: number | undefined | |||
| remarks: string | undefined | |||
| export interface QcInput { | |||
| id: number; | |||
| itemId: number; | |||
| acceptedQty: number; | |||
| demandQty: number; | |||
| status?: string; | |||
| jobOrderId: number; | |||
| purchaseOrderId?: number; | |||
| purchaseOrderLineId: number; | |||
| } | |||
| export interface QcFormInput { | |||
| acceptQty: number; | |||
| qcAccept: boolean; | |||
| qcDecision?: number; | |||
| qcResult: QcResult[]; | |||
| } | |||
| export const fetchQcItemCheckList = cache(async () => { | |||
| @@ -6,7 +6,7 @@ import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { QcResult } from "../qc"; | |||
| import { QcFormInput, QcResult } from "../qc"; | |||
| import { EscalationResult } from "../escalation"; | |||
| export enum StockInStatus { | |||
| @@ -156,14 +156,9 @@ export interface PutAwayInput { | |||
| warehouseId: number; | |||
| putAwayLines: PutAwayLine[] | |||
| } | |||
| export interface QcInput { | |||
| acceptQty: number; | |||
| qcAccept: boolean; | |||
| qcDecision?: number; | |||
| qcResult: QcResult[]; | |||
| } | |||
| export type ModalFormInput = Partial< | |||
| QcInput & StockInInput & PutAwayInput | |||
| QcFormInput & StockInInput & PutAwayInput | |||
| > & { | |||
| escalationLog? : Partial<EscalationInput> | |||
| }; | |||
| @@ -25,35 +25,36 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| //const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const [selectedDate, setSelectedDate] = useState<string>("today"); | |||
| const loadData = async (dateValue: string) => { | |||
| const loadSummaries = useCallback(async () => { | |||
| setIsLoadingSummary(true); | |||
| try { | |||
| let dateOffset = 0; | |||
| if (dateValue === "tomorrow") dateOffset = 1; | |||
| else if (dateValue === "dayAfterTomorrow") dateOffset = 2; | |||
| const requiredDate = dayjs().add(dateOffset, "day").format("YYYY-MM-DD"); | |||
| console.log("🔄 requiredDate:", requiredDate); | |||
| // Convert selectedDate to the format needed | |||
| let dateParam: string | undefined; | |||
| if (selectedDate === "today") { | |||
| dateParam = dayjs().format('YYYY-MM-DD'); | |||
| } else if (selectedDate === "tomorrow") { | |||
| dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||
| } else if (selectedDate === "dayAfterTomorrow") { | |||
| dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||
| } | |||
| const [s2, s4] = await Promise.all([ | |||
| fetchStoreLaneSummary("2/F", requiredDate), | |||
| fetchStoreLaneSummary("4/F", requiredDate), | |||
| fetchStoreLaneSummary("2/F", dateParam), | |||
| fetchStoreLaneSummary("4/F", dateParam) | |||
| ]); | |||
| console.log("🔄 s2:", s2); | |||
| console.log("🔄 s4:", s4); | |||
| setSummary2F(s2); | |||
| setSummary4F(s4); | |||
| } catch (e) { | |||
| console.error("load summaries failed:", e); | |||
| } catch (error) { | |||
| console.error("Error loading summaries:", error); | |||
| } finally { | |||
| setIsLoadingSummary(false); | |||
| } | |||
| }; | |||
| }, [selectedDate]); | |||
| // 初始化 | |||
| useEffect(() => { | |||
| loadData("today"); | |||
| }, []); | |||
| loadSummaries(); | |||
| }, [loadSummaries]); | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| @@ -72,7 +73,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| if (res.code === "SUCCESS") { | |||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadData(selectedDate); // 刷新按钮状态 | |||
| loadSummaries(); // 刷新按钮状态 | |||
| onPickOrderAssigned?.(); | |||
| } else if (res.code === "USER_BUSY") { | |||
| Swal.fire({ | |||
| @@ -141,9 +142,10 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||
| onChange={(e) => { { | |||
| setSelectedDate(e.target.value); | |||
| loadData(e.target.value); | |||
| loadSummaries(); | |||
| }}} | |||
| > | |||
| <MenuItem value="today"> | |||
| {t("Today")} ({getDateLabel(0)}) | |||
| </MenuItem> | |||
| @@ -16,7 +16,7 @@ import { Button, Stack } from "@mui/material"; | |||
| import { BomCombo } from "@/app/api/bom"; | |||
| import JoCreateFormModal from "./JoCreateFormModal"; | |||
| import AddIcon from '@mui/icons-material/Add'; | |||
| import QcStockInModal from "../PoDetail/QcStockInModal"; | |||
| import QcStockInModal from "../Qc/QcStockInModal"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { createStockInLine } from "@/app/api/stockIn/actions"; | |||
| @@ -57,11 +57,10 @@ import QrCodeIcon from "@mui/icons-material/QrCode"; | |||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | |||
| import { fetchQcResult } from "@/app/api/qc/actions"; | |||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||
| import DoDisturbIcon from "@mui/icons-material/DoDisturb"; | |||
| import { useSession } from "next-auth/react"; | |||
| // import { SessionWithTokens } from "src/config/authConfig"; | |||
| import QcStockInModal from "./QcStockInModal"; | |||
| import QcStockInModal from "../Qc/QcStockInModal"; | |||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { EscalationResult } from "@/app/api/escalation"; | |||
| @@ -256,31 +255,31 @@ function PoInputGrid({ | |||
| return await fetchQcResult(stockInLineId as number); | |||
| }, []); | |||
| const handleQC = useCallback( // UNUSED NOW! | |||
| (id: GridRowId, params: any) => async () => { | |||
| setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| const qcResult = await fetchQcDefaultValue(id); | |||
| // console.log(params.row); | |||
| console.log("Fetched QC Result:", qcResult); | |||
| // const handleQC = useCallback( // UNUSED NOW! | |||
| // (id: GridRowId, params: any) => async () => { | |||
| // setBtnIsLoading(true); | |||
| // setRowModesModel((prev) => ({ | |||
| // ...prev, | |||
| // [id]: { mode: GridRowModes.View }, | |||
| // })); | |||
| // const qcResult = await fetchQcDefaultValue(id); | |||
| // // console.log(params.row); | |||
| // console.log("Fetched QC Result:", qcResult); | |||
| setModalInfo({ | |||
| ...params.row, | |||
| qcResult: qcResult, | |||
| }); | |||
| // set default values | |||
| setTimeout(() => { | |||
| // open qc modal | |||
| console.log("delayed"); | |||
| openQcModal(); | |||
| setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [fetchQcDefaultValue, openQcModal], | |||
| ); | |||
| // setModalInfo({ | |||
| // ...params.row, | |||
| // qcResult: qcResult, | |||
| // }); | |||
| // // set default values | |||
| // setTimeout(() => { | |||
| // // open qc modal | |||
| // console.log("delayed"); | |||
| // openQcModal(); | |||
| // setBtnIsLoading(false); | |||
| // }, 200); | |||
| // }, | |||
| // [fetchQcDefaultValue, openQcModal], | |||
| // ); | |||
| const [newOpen, setNewOpen] = useState(false); | |||
| const stockInLineId = searchParams.get("stockInLineId"); | |||
| @@ -326,7 +325,7 @@ const closeNewModal = useCallback(() => { | |||
| // setTimeout(() => { | |||
| // }, 200); | |||
| }, | |||
| [fetchQcDefaultValue, openNewModal, pathname, router, searchParams] | |||
| [openNewModal, pathname, router, searchParams] | |||
| ); | |||
| // Open modal if `stockInLineId` exists in the URL | |||
| @@ -793,7 +792,7 @@ const closeNewModal = useCallback(() => { | |||
| }, | |||
| }, | |||
| ], | |||
| [t, handleStart, handleQC, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], | |||
| [t, handleStart, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], | |||
| ); | |||
| const unsortableColumns = useMemo(() => | |||
| @@ -1,482 +0,0 @@ | |||
| "use client"; | |||
| import { | |||
| ModalFormInput, | |||
| PurchaseQCInput, | |||
| PurchaseQcResult, | |||
| StockInInput, | |||
| StockInLineEntry, | |||
| updateStockInLine, | |||
| } from "@/app/api/po/actions"; | |||
| import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useContext, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import QcForm from "./QcForm"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { Check, CurrencyYuanRounded, TtyTwoTone } from "@mui/icons-material"; | |||
| import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; | |||
| import { useSearchParams } from "next/navigation"; | |||
| import { StockInLineRow } from "./PoInputGrid"; | |||
| import EscalationForm from "./EscalationForm"; | |||
| import StockInFormOld from "./StockInFormOld"; | |||
| import PutAwayForm from "./PutAwayForm"; | |||
| import { | |||
| INPUT_DATE_FORMAT, | |||
| stockInLineStatusMap, | |||
| } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | |||
| import UploadContext from "../UploadProvider/UploadProvider"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import RejectForm from "./RejectForm"; | |||
| import { isNullOrUndefined } from "html5-qrcode/esm/core"; | |||
| import { isEmpty, isFinite } from "lodash"; | |||
| dayjs.extend(arraySupport); | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| // setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||
| setStockInLine?: Dispatch<SetStateAction<StockInLine[]>>; | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
| setItemDetail: Dispatch< | |||
| SetStateAction< | |||
| | (StockInLine & { | |||
| warehouseId?: number; | |||
| }) | |||
| | undefined | |||
| > | |||
| >; | |||
| qc?: QcItemWithChecks[]; | |||
| warehouse?: any[]; | |||
| type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
| } | |||
| interface QcProps extends CommonProps { | |||
| qc: QcItemWithChecks[]; | |||
| type: "qc"; | |||
| } | |||
| interface StockInProps extends CommonProps { | |||
| // naming | |||
| type: "stockIn"; | |||
| } | |||
| interface PutawayProps extends CommonProps { | |||
| warehouse: any[]; | |||
| type: "putaway"; | |||
| } | |||
| interface EscalationProps extends CommonProps { | |||
| // naming | |||
| type: "escalation"; | |||
| } | |||
| interface RejectProps extends CommonProps { | |||
| // naming | |||
| type: "reject"; | |||
| } | |||
| type Props = | |||
| | QcProps | |||
| | StockInProps | |||
| | PutawayProps | |||
| | EscalationProps | |||
| | RejectProps; | |||
| 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%" }, | |||
| }; | |||
| const PoQcStockInModal: React.FC<Props> = ({ | |||
| type, | |||
| // setRows, | |||
| setEntries, | |||
| setStockInLine, | |||
| open, | |||
| onClose, | |||
| itemDetail, | |||
| setItemDetail, | |||
| qc, | |||
| warehouse, | |||
| }) => { | |||
| const { setIsUploading } = useUploadContext(); | |||
| const [serverError, setServerError] = useState(""); | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const params = useSearchParams(); | |||
| const [btnIsLoading, setBtnIsLoading] = useState(false); | |||
| // console.log(params.get("id")); | |||
| // console.log(itemDetail); | |||
| // console.log(itemDetail.qcResult); | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...itemDetail, | |||
| // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||
| // warehouseId: itemDetail.defaultWarehouseId || 0 | |||
| }, | |||
| }); | |||
| // console.log(formProps); | |||
| const errors = formProps.formState.errors; | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| useEffect(() => { | |||
| // setDefaultValues({...itemDetail}); | |||
| if (!itemDetail) { | |||
| console.log(itemDetail); | |||
| } | |||
| }, [itemDetail]); | |||
| // const fix0IndexedDate = useCallback((date: string | number[] | undefined) => { | |||
| // if (Array.isArray(date)) { | |||
| // console.log(date); | |||
| // return dayjs([date[0], date[1] - 1, date[2]]).format("YYYY-MM-DD"); | |||
| // } | |||
| // return date; | |||
| // }, []); | |||
| const accQty = formProps.watch("acceptedQty"); | |||
| useEffect(() => { | |||
| formProps.clearErrors("acceptedQty") | |||
| }, [accQty]) | |||
| const productLotNo = formProps.watch("productLotNo"); | |||
| const checkStockIn = useCallback( | |||
| (data: ModalFormInput): boolean => { | |||
| let hasErrors = false; | |||
| if (accQty! <= 0 ) { | |||
| formProps.setError("acceptedQty", { | |||
| message: `${t("Accepted qty must greater than")} ${ | |||
| 0 | |||
| }`, | |||
| type: "required", | |||
| }); | |||
| hasErrors = true; | |||
| } else if (accQty! > itemDetail.acceptedQty) { | |||
| formProps.setError("acceptedQty", { | |||
| message: `${t("Accepted qty must not greater than")} ${ | |||
| itemDetail.acceptedQty | |||
| }`, | |||
| type: "required", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (isEmpty(productLotNo)) { | |||
| formProps.setError("productLotNo", { | |||
| message: `${t("Product Lot No must not be empty")}`, | |||
| type: "required", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (itemDetail.shelfLife && !data.productionDate && !data.expiryDate) { | |||
| formProps.setError("productionDate", { | |||
| message: "Please provide at least one", | |||
| type: "invalid", | |||
| }); | |||
| formProps.setError("expiryDate", { | |||
| message: "Please provide at least one", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (!itemDetail.shelfLife && !data.expiryDate) { | |||
| formProps.setError("expiryDate", { | |||
| message: "Please provide expiry date", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (data.expiryDate && data.expiryDate < data.receiptDate!) { | |||
| formProps.setError("expiryDate", { | |||
| message: "Expired", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| return hasErrors; | |||
| }, | |||
| [accQty, itemDetail.acceptedQty, itemDetail.shelfLife, productLotNo, formProps, t], | |||
| ); | |||
| const checkPutaway = useCallback( | |||
| (data: ModalFormInput): boolean => { | |||
| let hasErrors = false; | |||
| console.log(data.warehouseId); | |||
| if (!data.warehouseId || data.warehouseId <= 0) { | |||
| formProps.setError("warehouseId", { | |||
| message: "Please provide warehouseId", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| return hasErrors; | |||
| }, | |||
| [itemDetail, formProps], | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| setBtnIsLoading(true); | |||
| setIsUploading(true); | |||
| formProps.clearErrors(); | |||
| let hasErrors = false; | |||
| console.log(errors); | |||
| console.log(data); | |||
| console.log(itemDetail); | |||
| // console.log(fix0IndexedDate(data.receiptDate)); | |||
| try { | |||
| // add checking | |||
| if (type === "stockIn") { | |||
| hasErrors = checkStockIn(data); | |||
| console.log(hasErrors); | |||
| } | |||
| if (type === "putaway") { | |||
| hasErrors = checkPutaway(data); | |||
| console.log(hasErrors); | |||
| } | |||
| //////////////////////// modify this mess later ////////////////////// | |||
| let productionDate = null; | |||
| let expiryDate = null; | |||
| let receiptDate = null; | |||
| let acceptedQty = null; | |||
| if (data.productionDate) { | |||
| productionDate = dayjs(data.productionDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| if (data.expiryDate) { | |||
| expiryDate = dayjs(data.expiryDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| if (data.receiptDate) { | |||
| receiptDate = dayjs(data.receiptDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| // if () | |||
| if (data.qcResult) { | |||
| acceptedQty = | |||
| itemDetail.acceptedQty - | |||
| data.qcResult.reduce((acc, curr) => acc + curr.failQty, 0); | |||
| } | |||
| const args = { | |||
| id: itemDetail.id, | |||
| purchaseOrderId: parseInt(params.get("id")!), | |||
| purchaseOrderLineId: itemDetail.purchaseOrderLineId, | |||
| itemId: itemDetail.itemId, | |||
| ...data, | |||
| productionDate: productionDate, | |||
| expiryDate: expiryDate, | |||
| receiptDate: receiptDate, | |||
| } as StockInLineEntry & ModalFormInput; | |||
| ////////////////////////////////////////////////////////////////////// | |||
| if (hasErrors) { | |||
| console.log(args); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| setBtnIsLoading(false); | |||
| setIsUploading(false); | |||
| return; | |||
| } | |||
| console.log(args); | |||
| // setBtnIsLoading(false); | |||
| // setIsUploading(false) | |||
| // return | |||
| const res = await updateStockInLine(args); | |||
| if (Boolean(res.id)) { | |||
| // update entries | |||
| const newEntries = res.entity as StockInLine[]; | |||
| console.log(newEntries); | |||
| if (setEntries) { | |||
| setEntries((prev) => { | |||
| const updatedEntries = [...prev]; // Create a new array | |||
| newEntries.forEach((item) => { | |||
| const index = updatedEntries.findIndex((p) => p.id === item.id); | |||
| if (index !== -1) { | |||
| // Update existing item | |||
| console.log(item); | |||
| updatedEntries[index] = item; | |||
| } else { | |||
| // Add new item | |||
| updatedEntries.push(item); | |||
| } | |||
| }); | |||
| return updatedEntries; // Return the new array | |||
| }); | |||
| } | |||
| if (setStockInLine) { | |||
| setStockInLine((prev) => { | |||
| const updatedEntries = [...prev]; // Create a new array | |||
| newEntries.forEach((item) => { | |||
| const index = updatedEntries.findIndex((p) => p.id === item.id); | |||
| if (index !== -1) { | |||
| // Update existing item | |||
| console.log(item); | |||
| updatedEntries[index] = item; | |||
| } else { | |||
| // Add new item | |||
| updatedEntries.push(item); | |||
| } | |||
| }); | |||
| return updatedEntries; // Return the new array | |||
| }); | |||
| } | |||
| // add loading | |||
| setBtnIsLoading(false); | |||
| setIsUploading(false); | |||
| setItemDetail(undefined); | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| console.log(res); | |||
| // if (res) | |||
| } catch (e) { | |||
| // server error | |||
| setBtnIsLoading(false); | |||
| setIsUploading(false); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } | |||
| }, | |||
| [setIsUploading, formProps, errors, itemDetail, type, params, checkStockIn, checkPutaway, t, setEntries, setStockInLine, setItemDetail, closeHandler], | |||
| ); | |||
| const printQrcode = useCallback(async () => { | |||
| setBtnIsLoading(true); | |||
| setIsUploading(true); | |||
| const postData = { stockInLineIds: [itemDetail.id] }; | |||
| // const postData = { stockInLineIds: [42,43,44] }; | |||
| const response = await fetchPoQrcode(postData); | |||
| if (response) { | |||
| console.log(response); | |||
| downloadFile(new Uint8Array(response.blobValue), response.filename!); | |||
| } | |||
| setBtnIsLoading(false); | |||
| setIsUploading(false); | |||
| }, [setIsUploading, itemDetail.id]); | |||
| const renderSubmitButton = useMemo((): boolean => { | |||
| if (itemDetail) { | |||
| const status = itemDetail.status; | |||
| console.log(status); | |||
| switch (type) { | |||
| case "qc": | |||
| return ( | |||
| stockInLineStatusMap[status] >= 1 && | |||
| stockInLineStatusMap[status] <= 2 | |||
| ); | |||
| case "escalation": | |||
| return ( | |||
| stockInLineStatusMap[status] === 1 || | |||
| stockInLineStatusMap[status] >= 3 || | |||
| stockInLineStatusMap[status] <= 5 | |||
| ); | |||
| case "stockIn": | |||
| return ( | |||
| stockInLineStatusMap[status] >= 3 && | |||
| stockInLineStatusMap[status] <= 6 | |||
| ); | |||
| case "putaway": | |||
| return stockInLineStatusMap[status] === 7; | |||
| case "reject": | |||
| return ( | |||
| stockInLineStatusMap[status] >= 1 && | |||
| stockInLineStatusMap[status] <= 6 | |||
| ); | |||
| default: | |||
| return false; // Handle unexpected type | |||
| } | |||
| } else return false; | |||
| }, [type, itemDetail]); | |||
| // useEffect(() => { | |||
| // console.log(renderSubmitButton) | |||
| // }, [renderSubmitButton]) | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler} sx={{ overflow: "scroll" }}> | |||
| <Box | |||
| sx={style} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| {itemDetail !== undefined && type === "qc" && ( | |||
| <QcForm | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={!renderSubmitButton} | |||
| /> | |||
| )} | |||
| {itemDetail !== undefined && type === "escalation" && ( | |||
| <EscalationForm | |||
| itemDetail={itemDetail} | |||
| disabled={!renderSubmitButton} | |||
| /> | |||
| )} | |||
| {itemDetail !== undefined && type === "stockIn" && ( | |||
| <StockInFormOld | |||
| itemDetail={itemDetail} | |||
| disabled={!renderSubmitButton} | |||
| /> | |||
| )} | |||
| {itemDetail !== undefined && type === "putaway" && ( | |||
| <PutAwayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={!renderSubmitButton} | |||
| /> | |||
| )} | |||
| {itemDetail !== undefined && type === "reject" && ( | |||
| <RejectForm | |||
| itemDetail={itemDetail} | |||
| disabled={!renderSubmitButton} | |||
| /> | |||
| )} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| {renderSubmitButton ? ( | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| disabled={btnIsLoading} | |||
| > | |||
| {t("submit")} | |||
| </Button> | |||
| ) : undefined} | |||
| {itemDetail !== undefined && type === "putaway" && ( | |||
| <Button | |||
| name="print" | |||
| variant="contained" | |||
| // startIcon={<Check />} | |||
| onClick={printQrcode} | |||
| disabled={btnIsLoading} | |||
| > | |||
| {t("print")} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoQcStockInModal; | |||
| @@ -53,7 +53,7 @@ import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { dummyPutAwayLine } from "./dummyQcTemplate"; | |||
| import { dummyPutAwayLine } from "../Qc/dummyQcTemplate"; | |||
| import { GridRowModesModel } from "@mui/x-data-grid"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -37,7 +37,7 @@ import { | |||
| GridApiCommunity, | |||
| GridSlotsComponentsProps, | |||
| } from "@mui/x-data-grid/internals"; | |||
| import { dummyQCData } from "./dummyQcTemplate"; | |||
| import { dummyQCData } from "../Qc/dummyQcTemplate"; | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| @@ -50,7 +50,7 @@ type EntryError = | |||
| type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||
| // fetchQcItemCheck | |||
| const QcForm: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| @@ -313,4 +313,4 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default QcForm; | |||
| export default QcFormOld; | |||
| @@ -1,24 +1,9 @@ | |||
| "use client"; | |||
| import { QcResult, QCInput } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Checkbox, | |||
| Collapse, | |||
| FormControl, | |||
| FormControlLabel, | |||
| Grid, | |||
| Radio, | |||
| RadioGroup, | |||
| Stack, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| TextField, | |||
| Tooltip, | |||
| Typography, | |||
| Box, Card, CardContent, Checkbox, Collapse, FormControl, | |||
| FormControlLabel, Grid, Radio, RadioGroup, Stack, Tab, | |||
| Tabs, TabsProps, TextField, Tooltip, Typography, | |||
| } from "@mui/material"; | |||
| import { useFormContext, Controller, FieldPath } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -36,31 +21,32 @@ import { | |||
| } from "@mui/x-data-grid"; | |||
| import InputDataGrid from "../InputDataGrid"; | |||
| import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||
| import TwoLineCell from "./TwoLineCell"; | |||
| import QcSelect from "./QcSelect"; | |||
| import TwoLineCell from "../PoDetail/TwoLineCell"; | |||
| import QcSelect from "../PoDetail/QcSelect"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { ModalFormInput, StockInLine } from "@/app/api/stockIn"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | |||
| import { fetchQcCategory, fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { QcCategory, QcData, QcInput, QcFormInput, QcResult } 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 EscalationComponent from "./EscalationComponent"; | |||
| import QcDataGrid from "./QCDatagrid"; | |||
| import EscalationComponent from "../PoDetail/EscalationComponent"; | |||
| import QcDataGrid from "../PoDetail/QCDatagrid"; | |||
| import { dummyEscalationHistory, | |||
| dummyQcData_A1, dummyQcData_E1, dummyQcData_E2, | |||
| dummyQcHeader_A1, dummyQcHeader_E1, dummyQcHeader_E2 } from "./dummyQcTemplate"; | |||
| import { escape, isNull, min } from "lodash"; | |||
| import { escape, isNull, min, template } from "lodash"; | |||
| import { PanoramaSharp } from "@mui/icons-material"; | |||
| import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | |||
| import { EscalationResult } from "@/app/api/escalation"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; | |||
| import CollapsibleCard from "../CollapsibleCard/CollapsibleCard"; | |||
| import LoadingComponent from "../General/LoadingComponent"; | |||
| import QcForm from "./QcForm"; | |||
| interface Props { | |||
| itemDetail: StockInLine; | |||
| itemDetail: QcInput; | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| // qc: QcItemWithChecks[]; | |||
| disabled: boolean; | |||
| @@ -74,7 +60,7 @@ type EntryError = | |||
| } | |||
| | undefined; | |||
| type QcRow = TableRow<Partial<QcData>, EntryError>; | |||
| type QcRow = TableRow<Partial<QcResult>, EntryError>; | |||
| // fetchQcItemCheck | |||
| const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| @@ -90,7 +76,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<QCInput>(); | |||
| } = useFormContext<QcFormInput>(); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>(); | |||
| @@ -98,6 +84,8 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| const qcAccept = watch("qcAccept"); | |||
| const qcDecision = watch("qcDecision"); //WIP | |||
| // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); | |||
| const [qcCategory, setQcCategory] = useState<QcCategory>(); | |||
| const qcRecord = useMemo(() => { // Need testing | |||
| const value = watch('qcResult'); //console.log("%c QC update!", "color:green", value); | |||
| @@ -105,30 +93,19 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| }, [watch('qcResult')]); | |||
| const [qcHistory, setQcHistory] = useState<QcResult[]>([]); | |||
| const [qcResult, setQcResult] = useState<QcResult[]>([]); | |||
| const [newQcData, setNewQcData] = useState<QcResult[]>([]); | |||
| const detailMode = useMemo(() => { | |||
| const isDetailMode = itemDetail.status == "escalated" || isNaN(itemDetail.jobOrderId); | |||
| return isDetailMode; | |||
| }, [itemDetail]); | |||
| const [escResult, setEscResult] = useState<EscalationResult[]>([]); | |||
| // const [qcAccept, setQcAccept] = useState(true); | |||
| // const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const column = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "escalation", | |||
| headerName: t("escalation"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "supervisor", | |||
| headerName: t("supervisor"), | |||
| flex: 1, | |||
| }, | |||
| ], [] | |||
| ) | |||
| const qcDisabled = (row : QcResult) => { | |||
| return disabled || isExist(row.escalationLogId); | |||
| }; | |||
| const isExist = (data : string | number | undefined) => { | |||
| return (data !== null && data !== undefined); | |||
| } | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| @@ -136,12 +113,23 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| [], | |||
| ); | |||
| const isExist = (data : string | number | undefined) => { | |||
| return (data !== null && data !== undefined); | |||
| } | |||
| const qcType = useMemo(() => { | |||
| if (itemDetail) { | |||
| const d = itemDetail; | |||
| if (isExist(d.jobOrderId)) { | |||
| return "EPQC"; | |||
| } | |||
| } | |||
| return "IQC"; // Default | |||
| }, [itemDetail]); | |||
| const detailMode = useMemo(() => { | |||
| const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId); | |||
| return isDetailMode; | |||
| }, [itemDetail]); | |||
| // W I P // | |||
| const validateFieldFail = (field : FieldPath<QCInput>, condition: boolean, message: string) : boolean => { | |||
| const validateFieldFail = (field : FieldPath<QcFormInput>, condition: boolean, message: string) : boolean => { | |||
| // console.log("Checking if " + message) | |||
| if (condition) { setError(field, { message: message}); return false; } | |||
| else { clearErrors(field); return true; } | |||
| @@ -166,8 +154,8 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| } else | |||
| console.log("%c Validated accQty:", "color:yellow", accQty); | |||
| } | |||
| },[setError, qcDecision, accQty, itemDetail]) | |||
| },[setError, qcDecision, accQty, itemDetail]) | |||
| useEffect(() => { // W I P // ----- | |||
| if (qcDecision == 1) { | |||
| if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ | |||
| @@ -213,208 +201,153 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| [], | |||
| ); | |||
| function BooleanEditCell(params: GridRenderEditCellParams) { | |||
| const apiRef = useGridApiContext(); | |||
| const { id, field, value } = params; | |||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||
| }; | |||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||
| } | |||
| const qcDisabled = (row : QcResult) => { | |||
| return disabled || isExist(row.escalationLogId); | |||
| }; | |||
| const qcColumns: GridColDef[] = useMemo(() => [ | |||
| { | |||
| field: "name", | |||
| headerName: t("qcItem"), | |||
| wrapText: true, | |||
| flex: 2.5, | |||
| renderCell: (params) => { | |||
| const index = params.api.getRowIndexRelativeToVisibleRows(params.id) + 1; | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| lineHeight: 1.5, | |||
| padding: "4px", | |||
| fontSize: 18, | |||
| }} | |||
| > | |||
| <b>{`${index}. ${params.value}`}</b><br/> | |||
| {params.row.description} | |||
| </Box> | |||
| )}, | |||
| }, | |||
| { | |||
| field: 'qcResult', | |||
| headerName: t("qcResult"), | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| const rowValue = params.row; | |||
| const index = Number(params.id);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| // console.log(rowValue.row); | |||
| return ( | |||
| <FormControl> | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| // defaultValue={""} | |||
| value={rowValue.qcPassed === undefined ? "" : (rowValue.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = (e.target.value === "true"); | |||
| // setQcItems((prev) => | |||
| // prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||
| // ); | |||
| setValue(`qcResult.${index}.qcPassed`, value); | |||
| }} | |||
| name={`qcPassed-${params.id}`} | |||
| > | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio />} | |||
| label="合格" | |||
| disabled={qcDisabled(rowValue)} | |||
| sx={{ | |||
| color: rowValue.qcPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio />} | |||
| label="不合格" | |||
| disabled={qcDisabled(rowValue)} | |||
| sx={{ | |||
| color: rowValue.qcPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| }} | |||
| /> | |||
| </RadioGroup> | |||
| </FormControl> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "failQty", | |||
| headerName: t("failedQty"), | |||
| flex: 0.5, | |||
| // editable: true, | |||
| renderCell: (params) => { | |||
| const index = Number(params.id);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| return ( | |||
| <TextField | |||
| type="number" | |||
| value={!params.row.qcPassed? params.value : '0'} | |||
| disabled={params.row.qcPassed || qcDisabled(params.row)} | |||
| /* TODO improve */ | |||
| /* Reference: https://grok.com/share/c2hhcmQtNA%3D%3D_10787069-3eec-40af-a7cc-bacbdb86bf05 */ | |||
| onChange={(e) => { | |||
| const v = e.target.value; | |||
| const next = v === '' ? undefined : Number(v); | |||
| if (Number.isNaN(next)) return; | |||
| setValue(`qcResult.${index}.failQty`, next); | |||
| }} | |||
| // onBlur={(e) => { | |||
| // const v = e.target.value; | |||
| // const next = v === '' ? undefined : Number(v); | |||
| // if (Number.isNaN(next)) return; | |||
| // setValue(`qcResult.${index}.failQty`, next); | |||
| // }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: '100%', | |||
| "& .MuiInputBase-input": { | |||
| padding: "0.75rem", | |||
| fontSize: 24, | |||
| }, | |||
| }} | |||
| /> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "remarks", | |||
| headerName: t("remarks"), | |||
| flex: 2, | |||
| renderCell: (params) => { | |||
| const index = Number(params.id);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| return ( | |||
| <TextField | |||
| size="small" | |||
| defaultValue={params.value} | |||
| disabled={qcDisabled(params.row)} | |||
| onBlur={(e) => { | |||
| const value = e.target.value; | |||
| setValue(`qcResult.${index}.remarks`, value); | |||
| }} | |||
| // onChange={(e) => { | |||
| // const remarks = e.target.value; | |||
| // // const next = v === '' ? undefined : Number(v); | |||
| // // if (Number.isNaN(next)) return; | |||
| // // setQcItems((prev) => | |||
| // // prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) | |||
| // // ); | |||
| // }} | |||
| // {...register(`qcResult.${index}.remarks`, { | |||
| // required: "remarks required!", | |||
| // })} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| sx={{ width: '100%', | |||
| "& .MuiInputBase-input": { | |||
| padding: "0.75rem", | |||
| fontSize: 24, | |||
| }, | |||
| }} | |||
| /> | |||
| ); | |||
| }, | |||
| }, | |||
| ], []) | |||
| // Set initial value for acceptQty | |||
| useEffect(() => { | |||
| if (itemDetail?.demandQty > 0) { //!== undefined) { | |||
| setValue("acceptQty", itemDetail.demandQty); // THIS NEED TO UPDATE TO NOT USE DEMAND QTY | |||
| setValue("acceptQty", itemDetail.demandQty); // TODO: THIS NEED TO UPDATE TO NOT USE DEMAND QTY | |||
| } else { | |||
| setValue("acceptQty", itemDetail?.acceptedQty); | |||
| } | |||
| }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | |||
| // Fetch Qc Data | |||
| useEffect(() => { | |||
| console.log("%c Qc Record updated:", "color:green", qcRecord); | |||
| if (qcRecord.length < 1) { // New QC | |||
| const fetchedQcData = dummyQcData; //TODO fetch from DB | |||
| setValue("qcResult", fetchedQcData); | |||
| } else { | |||
| if (itemDetail?.status == "escalated") { // Copy the previous QC data for editing | |||
| if (qcRecord.find((qc) => !isExist(qc.escalationLogId)) === undefined) { | |||
| const copiedQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined })); | |||
| const mutableQcData = [...qcRecord, ...copiedQcData]; | |||
| setValue("qcResult", mutableQcData); | |||
| } | |||
| // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||
| if (itemDetail) { | |||
| const d = itemDetail; | |||
| fetchNewQcData(d); | |||
| if (d.status == "pending") { | |||
| // | |||
| } else { | |||
| fetchQcResultData(d); | |||
| } | |||
| } | |||
| }, [itemDetail]); | |||
| const fetchNewQcData = useCallback( | |||
| async (input: QcInput) => { | |||
| try { | |||
| const res = await fetchQcCategory(input.itemId, qcType); | |||
| if (res.qcItems.length > 0) { | |||
| console.log("%c Fetched Qc Template: ", "color:orange", res); | |||
| setQcCategory(res); | |||
| // setQcResult(res.qcItems); | |||
| // setValue("qcResult", res.qcItems); | |||
| } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching Qc Template: ", "color:red", e); | |||
| alert(t("Missing QC Template, please contact administrator")); | |||
| // closeHandler({}, "backdropClick"); | |||
| } | |||
| },[fetchQcCategory, setValue] | |||
| ); | |||
| const fetchQcResultData = useCallback( | |||
| async (input: QcInput) => { | |||
| try { | |||
| const res = await fetchQcResult(input.id); // StockInLineId for now | |||
| if (res.length > 0) { | |||
| console.log("%c Fetched Qc Result: ", "color:orange", res); | |||
| setValue("qcResult", res); | |||
| fetchEscalationLogData(input.id); | |||
| // } else {setStockInLineInfo((prev) => ({...prev, qcResult: []} as StockInLine));} | |||
| } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching Qc Result: ", "color:red", e); | |||
| // alert("Something went wrong, please retry"); | |||
| // closeHandler({}, "backdropClick"); | |||
| } | |||
| },[fetchQcResult, setValue] | |||
| ); | |||
| const fetchEscalationLogData = useCallback( | |||
| async (stockInLineId: number) => { | |||
| try { | |||
| const res = await fetchEscalationLogsByStockInLines([stockInLineId]); | |||
| if (res.length > 0) { | |||
| console.log("%c Fetched Escalation Log: ", "color:orange", res[0]); | |||
| setEscResult(res); | |||
| // formProps.setValue("escalationLog", res[0]); | |||
| }// else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching EscalationLog: ", "color:red", e); | |||
| // alert("Something went wrong, please retry"); | |||
| // closeHandler({}, "backdropClick"); | |||
| } | |||
| },[fetchEscalationLogsByStockInLines] | |||
| ); | |||
| if (qcRecord.length > 0) { | |||
| if (qcResult.length < 1) { // Set QC Result | |||
| // Set QC Data | |||
| useEffect(() => { | |||
| if (itemDetail) { | |||
| const d = itemDetail; | |||
| if (qcRecord.length < 1) { // No QC Data | |||
| if (d.status == "pending") { // New QC | |||
| if (qcCategory) { | |||
| if (qcCategory.qcItems.length > 0) { | |||
| const filledQcItems = fillQcResult(qcCategory.qcItems); | |||
| setValue("qcResult", filledQcItems); | |||
| console.log("%c New QC Record applied:", "color:green", filledQcItems); | |||
| } | |||
| } | |||
| } else { | |||
| console.log("%c No QC Record loaded:", "color:green"); | |||
| // | |||
| } | |||
| } else { // QC Result fetched | |||
| if (qcRecord.some(qc => qc.order !== undefined)) { // If QC Result is filled with order | |||
| if (d.status == "escalated") { // Copy the previous QC data for editing | |||
| // If no editable Qc Data | |||
| if (!qcRecord.some((qc) => !isExist(qc.escalationLogId))) { | |||
| const mutableQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined })); | |||
| const copiedQcData = [...mutableQcData, ...qcRecord]; | |||
| setValue("qcResult", copiedQcData); | |||
| console.log("%c QC Record copied:", "color:green", copiedQcData); | |||
| return; | |||
| } | |||
| } | |||
| // Set QC Result | |||
| // const filteredQcResult = qcRecord; | |||
| const filteredQcResult = qcRecord.filter((qc) => !isExist(qc.escalationLogId)); | |||
| console.log("%c QC Result loaded:", "color:green", filteredQcResult); | |||
| setQcResult(filteredQcResult); | |||
| } | |||
| if (qcHistory.length < 1) { // Set QC History | |||
| const filteredQcHistory = qcRecord.filter((qc) => isExist(qc.escalationLogId)); | |||
| setQcHistory(filteredQcHistory); | |||
| // Set QC History | |||
| if (filteredQcResult.length < qcRecord.length) { // If there are Qc History | |||
| if (qcHistory.length < 1) { | |||
| const filteredQcHistory = qcRecord.filter((qc) => isExist(qc.escalationLogId)); | |||
| console.log("%c QC History loaded:", "color:green", filteredQcHistory); | |||
| setQcHistory(filteredQcHistory); | |||
| } | |||
| } | |||
| } else { | |||
| if (qcCategory) { | |||
| const filledQcData = fillQcResult(qcRecord, qcCategory?.qcItems); | |||
| console.log("%c QC Result filled:", "color:green", filledQcData); | |||
| setValue("qcResult", filledQcData); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }, [qcRecord, setValue]) | |||
| }, [qcRecord, qcCategory, setValue, itemDetail]) | |||
| const fillQcResult = (qcResults: QcResult[], qcItems: QcData[] = []) => { | |||
| let result = [] as QcResult[]; | |||
| qcResults.forEach((r, index) => { | |||
| const target = qcItems.find((t) => t.qcItemId === r.qcItemId); | |||
| const n = { ...target, ...r }; //, id: index }; | |||
| result.push(n); | |||
| }); | |||
| result.sort((a,b) => a.order! - b.order!); | |||
| return result; | |||
| }; | |||
| // const [openCollapse, setOpenCollapse] = useState(false) | |||
| const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | |||
| @@ -435,11 +368,6 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| // }, [setValue]); | |||
| useEffect(() => { | |||
| // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||
| }, [itemDetail]); | |||
| const setDefaultQcDecision = (status : string | undefined) => { | |||
| const param = status?.toLowerCase(); | |||
| if (param !== undefined && param !== null) { | |||
| @@ -473,37 +401,46 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| } else { return 60} | |||
| }; | |||
| // For DEMO | |||
| const dummyQcData = useMemo(() => { | |||
| const d = itemDetail; | |||
| if (d.itemId == 23239 || d.itemNo == "PP2277" || d.itemName == "烚意粉") { | |||
| return dummyQcData_E2; | |||
| } else { | |||
| if (d.jobOrderId === null) { | |||
| return dummyQcData_A1; | |||
| } else { | |||
| return dummyQcData_E1; | |||
| } | |||
| } | |||
| }, [itemDetail]) | |||
| const formattedDesc = (content: string = "") => { | |||
| return ( | |||
| <> | |||
| {content.split("\\n").map((line, index) => ( | |||
| <span key={index}> {line} <br/></span> | |||
| ))} | |||
| </> | |||
| ); | |||
| } | |||
| const dummyQcHeader = useMemo(() => { | |||
| const d = itemDetail; | |||
| if (d.itemId == 23239 || d.itemNo == "PP2277" || d.itemName == "烚意粉") { | |||
| return dummyQcHeader_E2; | |||
| } else { | |||
| if (d.jobOrderId === null) { | |||
| return dummyQcHeader_A1; | |||
| } else { | |||
| return dummyQcHeader_E1; | |||
| } | |||
| } | |||
| }, [itemDetail]) | |||
| const QcHeader = useMemo(() => () => { | |||
| if (qcCategory === undefined || qcCategory === null) { | |||
| return ( | |||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
| N/A | |||
| </Typography> | |||
| ); | |||
| } else | |||
| return ( | |||
| <> | |||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
| {qcCategory?.name} ({qcCategory?.code}) | |||
| </Typography> | |||
| <Typography variant="subtitle1" sx={{ color: '#666' }}> | |||
| <b>品檢類型</b>:{qcType} | |||
| </Typography> | |||
| <Typography variant="subtitle2" sx={{ color: '#666' }}> | |||
| {formattedDesc(qcCategory?.description)} | |||
| </Typography> | |||
| </Box> | |||
| </> | |||
| ); | |||
| }, [qcType, qcCategory]); | |||
| return ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| {itemDetail ? ( | |||
| {(qcRecord.length > 0) ? ( | |||
| // {(qcRecord.length > 0 && qcCategory) ? ( | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| @@ -518,41 +455,23 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| variant="scrollable" | |||
| > | |||
| <Tab label={t("QC Info")} iconPosition="end" /> | |||
| {(itemDetail.escResult && itemDetail.escResult?.length > 0) && | |||
| {(escResult && escResult?.length > 0) && | |||
| (<Tab label={t("Escalation History")} iconPosition="end" />)} | |||
| </Tabs> | |||
| </Grid> | |||
| {tabIndex == 0 && ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
| {dummyQcHeader.name} | |||
| </Typography> | |||
| <Typography variant="subtitle1" sx={{ color: '#666' }}> | |||
| <b>品檢類型</b>:{dummyQcHeader.type} | |||
| </Typography> | |||
| <Typography variant="subtitle2" sx={{ color: '#666' }}> | |||
| {dummyQcHeader.description} | |||
| </Typography> | |||
| </Box> | |||
| <QcHeader/> | |||
| {/* <QcDataGrid<ModalFormInput, QcData, EntryError> | |||
| apiRef={apiRef} | |||
| columns={qcColumns} | |||
| _formKey="qcResult" | |||
| validateRow={validation} | |||
| /> */} | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| <QcForm | |||
| rows={qcResult} | |||
| // rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||
| // rows={disabled? qcResult:qcItems} | |||
| // autoHeight | |||
| sortModel={[]} | |||
| // getRowHeight={getRowHeight} | |||
| getRowHeight={() => 'auto'} | |||
| getRowId={getRowId} | |||
| disabled={disabled} | |||
| /> | |||
| </Grid> | |||
| </> | |||
| @@ -571,26 +490,12 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| </Typography> | |||
| </Grid> */} | |||
| <Grid item xs={12}> | |||
| <EscalationLogTable type="qc" items={itemDetail.escResult || []}/> | |||
| <EscalationLogTable type="qc" items={escResult || []}/> | |||
| <CollapsibleCard title={t("QC Record")}> | |||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
| {dummyQcHeader.name} | |||
| </Typography> | |||
| <Typography variant="subtitle1" sx={{ color: '#666' }}> | |||
| <b>品檢類型</b>:{dummyQcHeader.type} | |||
| </Typography> | |||
| <Typography variant="subtitle2" sx={{ color: '#666' }}> | |||
| {dummyQcHeader.description} | |||
| </Typography> | |||
| </Box> | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| <QcHeader/> | |||
| <QcForm | |||
| disabled={disabled} | |||
| rows={qcHistory} | |||
| // rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||
| // rows={disabled? qcResult:qcItems} | |||
| autoHeight | |||
| sortModel={[]} | |||
| /> | |||
| </CollapsibleCard> | |||
| </Grid> | |||
| @@ -712,7 +617,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| <FormControlLabel disabled={disabled} | |||
| value="3" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||
| label="上報品檢結果" /> | |||
| label="暫時存放到置物區,並等待品檢結果" /> | |||
| </>)} | |||
| </RadioGroup> | |||
| </> | |||
| @@ -730,18 +635,6 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| setIsCollapsed={setIsCollapsed} | |||
| /> | |||
| </Grid>)} | |||
| {/* {qcAccept && <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Escalation Result")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <EscalationComponent | |||
| forSupervisor={true} | |||
| isCollapsed={isCollapsed} | |||
| setIsCollapsed={setIsCollapsed} | |||
| /> | |||
| </Grid>} */} | |||
| </Grid> | |||
| ) : <LoadingComponent/>} | |||
| </Grid> | |||
| @@ -0,0 +1,245 @@ | |||
| "use client"; | |||
| import { | |||
| Box, Card, CardContent, Checkbox, Collapse, FormControl, | |||
| FormControlLabel, Grid, Radio, RadioGroup, Stack, Tab, | |||
| Tabs, TabsProps, TextField, Tooltip, Typography, | |||
| } from "@mui/material"; | |||
| import { useFormContext, Controller, FieldPath } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { | |||
| GridColDef, | |||
| useGridApiContext, | |||
| GridRenderEditCellParams, | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import { QcFormInput, QcResult } from "@/app/api/qc"; | |||
| interface Props { | |||
| rows: QcResult[]; | |||
| disabled?: boolean; | |||
| } | |||
| const QcForm: React.FC<Props> = ({ rows, disabled = false }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| reset, | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<QcFormInput>(); | |||
| const qcDisabled = (row : QcResult) => { | |||
| return disabled || isExist(row.escalationLogId); | |||
| }; | |||
| const isExist = (data : string | number | undefined) => { | |||
| return (data !== null && data !== undefined); | |||
| } | |||
| function BooleanEditCell(params: GridRenderEditCellParams) { | |||
| const apiRef = useGridApiContext(); | |||
| const { id, field, value } = params; | |||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||
| }; | |||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||
| } | |||
| const qcColumns: GridColDef[] = useMemo(() => [ | |||
| { | |||
| field: "name", | |||
| headerName: t("qcItem"), | |||
| wrapText: true, | |||
| flex: 2.5, | |||
| renderCell: (params) => { | |||
| const index = getRowIndex(params);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| lineHeight: 1.5, | |||
| padding: "4px", | |||
| fontSize: 18, | |||
| }} | |||
| > | |||
| <b>{`${params.row.order ?? "N/A"}. ${params.value}`}</b><br/> | |||
| {params.row.description} | |||
| </Box> | |||
| )}, | |||
| }, | |||
| { | |||
| field: 'qcResult', | |||
| headerName: t("qcResult"), | |||
| flex: 1, | |||
| renderCell: (params) => { | |||
| const rowValue = params.row; | |||
| const index = getRowIndex(params);//params.api.getRowIndexRelativeToVisibleRows(params.row.id); | |||
| // const index = Number(params.id); | |||
| // const index = Number(params.row.order - 1); | |||
| // console.log(rowValue.row); | |||
| return ( | |||
| <FormControl> | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| // defaultValue={""} | |||
| value={rowValue.qcPassed === undefined ? "" : (rowValue.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = (e.target.value === "true"); | |||
| // setQcItems((prev) => | |||
| // prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||
| // ); | |||
| setValue(`qcResult.${index}.qcPassed`, value); | |||
| }} | |||
| name={`qcPassed-${params.id}`} | |||
| > | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio />} | |||
| label="合格" | |||
| disabled={qcDisabled(rowValue)} | |||
| sx={{ | |||
| color: rowValue.qcPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio />} | |||
| label="不合格" | |||
| disabled={qcDisabled(rowValue)} | |||
| sx={{ | |||
| color: rowValue.qcPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| }} | |||
| /> | |||
| </RadioGroup> | |||
| </FormControl> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "failQty", | |||
| headerName: t("failedQty"), | |||
| flex: 0.5, | |||
| // editable: true, | |||
| renderCell: (params) => { | |||
| const index = getRowIndex(params);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| // const index = Number(params.id); | |||
| return ( | |||
| <TextField | |||
| type="number" | |||
| value={!params.row.qcPassed? params.value : '0'} | |||
| disabled={params.row.qcPassed || qcDisabled(params.row)} | |||
| /* TODO improve */ | |||
| /* Reference: https://grok.com/share/c2hhcmQtNA%3D%3D_10787069-3eec-40af-a7cc-bacbdb86bf05 */ | |||
| onChange={(e) => { | |||
| const v = e.target.value; | |||
| const next = v === '' ? undefined : Number(v); | |||
| if (Number.isNaN(next)) return; | |||
| setValue(`qcResult.${index}.failQty`, next); | |||
| }} | |||
| // onBlur={(e) => { | |||
| // const v = e.target.value; | |||
| // const next = v === '' ? undefined : Number(v); | |||
| // if (Number.isNaN(next)) return; | |||
| // setValue(`qcResult.${index}.failQty`, next); | |||
| // }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: '100%', | |||
| "& .MuiInputBase-input": { | |||
| padding: "0.75rem", | |||
| fontSize: 24, | |||
| }, | |||
| }} | |||
| /> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "remarks", | |||
| headerName: t("remarks"), | |||
| flex: 2, | |||
| renderCell: (params) => { | |||
| // const index = Number(params.id);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| const index = getRowIndex(params);//params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| return ( | |||
| <TextField | |||
| size="small" | |||
| defaultValue={params.value} | |||
| disabled={qcDisabled(params.row)} | |||
| onBlur={(e) => { | |||
| const value = e.target.value; | |||
| setValue(`qcResult.${index}.remarks`, value); | |||
| }} | |||
| // onChange={(e) => { | |||
| // const remarks = e.target.value; | |||
| // // const next = v === '' ? undefined : Number(v); | |||
| // // if (Number.isNaN(next)) return; | |||
| // // setQcItems((prev) => | |||
| // // prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) | |||
| // // ); | |||
| // }} | |||
| // {...register(`qcResult.${index}.remarks`, { | |||
| // required: "remarks required!", | |||
| // })} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| sx={{ width: '100%', | |||
| "& .MuiInputBase-input": { | |||
| padding: "0.75rem", | |||
| fontSize: 24, | |||
| }, | |||
| }} | |||
| /> | |||
| ); | |||
| }, | |||
| }, | |||
| ], []) | |||
| // const getRowId = (row :any) => { | |||
| // return qcRecord.findIndex(qc => qc == row); | |||
| // // return row.id || `${row.name}-${Math.random().toString(36).substr(2, 9)}`; | |||
| // }; | |||
| const getRowHeight = (row :any) => { // Not used? | |||
| console.log("row", row); | |||
| if (!row.model.name) { | |||
| return (row.model.name.length ?? 10) * 1.2 + 30; | |||
| } else { return 60} | |||
| }; | |||
| const getRowIndex = (params: any) => { | |||
| return params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
| // return params.row.id; | |||
| } | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| rows={rows} | |||
| // autoHeight | |||
| sortModel={[]} | |||
| getRowHeight={() => 'auto'} | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| export default QcForm; | |||
| @@ -0,0 +1,669 @@ | |||
| "use client"; | |||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | |||
| import { | |||
| Autocomplete, | |||
| Box, | |||
| Button, | |||
| Divider, | |||
| Grid, | |||
| Modal, | |||
| ModalProps, | |||
| Stack, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| TextField, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { StockInLineRow } from "../PoDetail/PoInputGrid"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StockInForm from "../StockIn/StockInForm"; | |||
| import QcComponent from "./QcComponent"; | |||
| import PutAwayForm from "../PoDetail/PutAwayForm"; | |||
| import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | |||
| import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | |||
| import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | |||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { EscalationResult } from "@/app/api/escalation"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { GridRowModesModel } from "@mui/x-data-grid"; | |||
| import { isEmpty } from "lodash"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { truncateSync } from "fs"; | |||
| import { ModalFormInput, StockInLineInput, StockInLine } from "@/app/api/stockIn"; | |||
| import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | |||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | |||
| import FgStockInForm from "../StockIn/FgStockInForm"; | |||
| import LoadingComponent from "../General/LoadingComponent"; | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| bgcolor: "background.paper", | |||
| // pt: 5, | |||
| // px: 5, | |||
| // pb: 10, | |||
| display: "block", | |||
| width: { xs: "90%", sm: "90%", md: "90%" }, | |||
| height: { xs: "90%", sm: "90%", md: "90%" }, | |||
| }; | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] } | undefined; | |||
| inputDetail: StockInLineInput | undefined; | |||
| session: SessionWithTokens | null; | |||
| warehouse?: any[]; | |||
| printerCombo: PrinterCombo[]; | |||
| onClose: () => void; | |||
| skipQc?: Boolean; | |||
| } | |||
| interface Props extends CommonProps { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| const QcStockInModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| // itemDetail, | |||
| inputDetail, | |||
| session, | |||
| warehouse, | |||
| printerCombo, | |||
| skipQc = false, | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const [stockInLineInfo, setStockInLineInfo] = useState<StockInLine>(); | |||
| const [isLoading, setIsLoading] = useState<Boolean>(false); | |||
| // const [skipQc, setSkipQc] = useState<Boolean>(false); | |||
| // const [viewOnly, setViewOnly] = useState(false); | |||
| // Select Printer | |||
| const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); | |||
| const [printQty, setPrintQty] = useState(1); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| const fetchStockInLineData = useCallback( | |||
| async (stockInLineId: number) => { | |||
| try { | |||
| const res = await fetchStockInLineInfo(stockInLineId); | |||
| if (res) { | |||
| console.log("%c Fetched Stock In Line: ", "color:orange", res); | |||
| setStockInLineInfo({...inputDetail, ...res, expiryDate: inputDetail?.expiryDate}); // TODO review to overwrite res with inputDetail instead (revise PO fetching data) | |||
| // fetchQcResultData(stockInLineId); | |||
| } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching Stock In Line: ", "color:red", e); | |||
| alert("Something went wrong, please retry"); | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| },[fetchStockInLineInfo, inputDetail] | |||
| ); | |||
| // Fetch info if id is input | |||
| useEffect(() => { | |||
| setIsLoading(true); | |||
| if (inputDetail && open) { | |||
| console.log("%c Opened Modal with input:", "color:yellow", inputDetail); | |||
| if (inputDetail.id) { | |||
| const id = inputDetail.id; | |||
| fetchStockInLineData(id); | |||
| } | |||
| } | |||
| }, [open]); | |||
| // Make sure stock in line info is fetched | |||
| useEffect(() => { | |||
| if (stockInLineInfo) { | |||
| if (stockInLineInfo.id) { | |||
| if (isLoading) { | |||
| formProps.reset({ | |||
| ...defaultNewValue | |||
| }); | |||
| console.log("%c Modal loaded successfully", "color:lime"); | |||
| setIsLoading(false); | |||
| } | |||
| } | |||
| } | |||
| }, [stockInLineInfo]); | |||
| const defaultNewValue = useMemo(() => { | |||
| const d = stockInLineInfo; | |||
| if (d !== undefined) { | |||
| // console.log("%c sil info", "color:yellow", d ) | |||
| return ( | |||
| { | |||
| ...d, | |||
| // status: d.status ?? "pending", | |||
| productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : undefined, | |||
| expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined, | |||
| receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") | |||
| : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | |||
| acceptQty: d.demandQty?? d.acceptedQty, | |||
| // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], | |||
| // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], | |||
| warehouseId: d.defaultWarehouseId ?? 1, | |||
| putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [], | |||
| } as ModalFormInput | |||
| ) | |||
| } return undefined | |||
| }, [stockInLineInfo]) | |||
| // const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...defaultNewValue, | |||
| }, | |||
| }); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| () => { | |||
| setStockInLineInfo(undefined); | |||
| formProps.reset({}); | |||
| onClose?.(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const isPutaway = () => { | |||
| if (stockInLineInfo) { | |||
| const status = stockInLineInfo.status; | |||
| return status == "received"; | |||
| } else return false; | |||
| }; | |||
| // Get show putaway | |||
| const showPutaway = useMemo(() => { | |||
| if (stockInLineInfo) { | |||
| const status = stockInLineInfo.status; | |||
| return status !== "pending" && status !== "escalated" && status !== "rejected"; | |||
| } | |||
| return false; | |||
| }, [stockInLineInfo]); | |||
| // Get is view only | |||
| const viewOnly = useMemo(() => { | |||
| if (stockInLineInfo) { | |||
| if (stockInLineInfo.status) { | |||
| const status = stockInLineInfo.status; | |||
| const isViewOnly = status.toLowerCase() == "completed" | |||
| || status.toLowerCase() == "partially_completed" // TODO update DB | |||
| || status.toLowerCase() == "rejected" | |||
| || (status.toLowerCase() == "escalated" && session?.id != stockInLineInfo.handlerId) | |||
| if (showPutaway) { setTabIndex(1); } else { setTabIndex(0); } | |||
| return isViewOnly; | |||
| } | |||
| } | |||
| return true; | |||
| }, [stockInLineInfo]) | |||
| const [openPutaway, setOpenPutaway] = useState(false); | |||
| const onOpenPutaway = useCallback(() => { | |||
| setOpenPutaway(true); | |||
| }, []); | |||
| const onClosePutaway = useCallback(() => { | |||
| setOpenPutaway(false); | |||
| }, []); | |||
| // Stock In submission handler | |||
| const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("Stock In Submission:", event!.nativeEvent); | |||
| // Extract only stock-in related fields | |||
| const stockInData = { | |||
| // quantity: data.quantity, | |||
| // receiptDate: data.receiptDate, | |||
| // batchNumber: data.batchNumber, | |||
| // expiryDate: data.expiryDate, | |||
| // warehouseId: data.warehouseId, | |||
| // location: data.location, | |||
| // unitCost: data.unitCost, | |||
| data: data, | |||
| // Add other stock-in specific fields from your form | |||
| }; | |||
| console.log("Stock In Data:", stockInData); | |||
| // Handle stock-in submission logic here | |||
| // e.g., call API, update state, etc. | |||
| }, | |||
| [], | |||
| ); | |||
| // QC submission handler | |||
| const onSubmitErrorQc = useCallback<SubmitErrorHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("Error", data); | |||
| }, [] | |||
| ); | |||
| // QC submission handler | |||
| const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("QC Submission:", event!.nativeEvent); | |||
| console.log("Validating:", data.qcResult); | |||
| // TODO: Move validation into QC page | |||
| // if (errors.length > 0) { | |||
| // alert(`未完成品檢: ${errors.map((err) => err[1].message)}`); | |||
| // return; | |||
| // } | |||
| // Get QC data from the shared form context | |||
| const qcAccept = data.qcDecision == 1; | |||
| // const qcAccept = data.qcAccept; | |||
| let acceptQty = Number(data.acceptQty); | |||
| const qcResults = data.qcResult?.filter((qc) => qc.escalationLogId === undefined) || []; // Remove old QC data | |||
| // const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems; | |||
| // const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems; | |||
| // Validate QC data | |||
| const validationErrors : string[] = []; | |||
| // Check if failed items have failed quantity | |||
| const failedItemsWithoutQty = qcResults.filter(item => | |||
| item.qcPassed === false && (!item.failQty || item.failQty <= 0) | |||
| ); | |||
| if (failedItemsWithoutQty.length > 0) { | |||
| validationErrors.push(`${t("Failed items must have failed quantity")}`); | |||
| // validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(', ')}`); | |||
| } | |||
| // Check if QC accept decision is made | |||
| if (data.qcDecision === undefined) { | |||
| // if (qcAccept === undefined) { | |||
| validationErrors.push(t("QC decision is required")); | |||
| } | |||
| // Check if accept quantity is valid | |||
| if (data.qcDecision == 2) { | |||
| acceptQty = 0; | |||
| } else { | |||
| if (acceptQty === undefined || acceptQty <= 0) { | |||
| validationErrors.push("Accept quantity must be greater than 0"); | |||
| } | |||
| } | |||
| // Check if dates are input | |||
| // if (data.productionDate === undefined || data.productionDate == null) { | |||
| // alert("請輸入生產日期!"); | |||
| // return; | |||
| // } | |||
| if (data.expiryDate === undefined || data.expiryDate == null) { | |||
| alert("請輸入到期日!"); | |||
| return; | |||
| } | |||
| if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && stockInLineInfo?.status != "escalated") { //TODO: fix it please! | |||
| validationErrors.push("有不合格檢查項目,無法收貨!"); | |||
| // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| // return; | |||
| } | |||
| // Check if all QC items have results | |||
| const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined); | |||
| if (itemsWithoutResult.length > 0 && stockInLineInfo?.status != "escalated") { //TODO: fix it please! | |||
| validationErrors.push(`${t("QC items without result")}`); | |||
| // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||
| } | |||
| if (validationErrors.length > 0 && !skipQc) { | |||
| console.error("QC Validation failed:", validationErrors); | |||
| alert(`未完成品檢: ${validationErrors}`); | |||
| return; | |||
| } | |||
| const qcData = { | |||
| dnNo : data.dnNo? data.dnNo : "DN00000", | |||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| productionDate : arrayToDateString(data.productionDate, "input"), | |||
| expiryDate : arrayToDateString(data.expiryDate, "input"), | |||
| receiptDate : arrayToDateString(data.receiptDate, "input"), | |||
| qcAccept: qcAccept? qcAccept : false, | |||
| acceptQty: acceptQty? acceptQty : 0, | |||
| // qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ | |||
| qcResult: qcResults.map(item => ({ | |||
| // id: item.id, | |||
| qcItemId: item.qcItemId, | |||
| // code: item.code, | |||
| // qcDescription: item.qcDescription, | |||
| qcPassed: item.qcPassed? item.qcPassed : false, | |||
| failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | |||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | |||
| remarks: item.remarks || '', | |||
| })), | |||
| }; | |||
| // const qcData = data; | |||
| console.log("QC Data for submission:", qcData); | |||
| if (data.qcDecision == 3) { // Escalate | |||
| if (data.escalationLog?.handlerId == undefined) { alert("請選擇上報負責同事!"); return; } | |||
| else if (data.escalationLog?.handlerId < 1) { alert("上報負責同事資料有誤"); return; } | |||
| const escalationLog = { | |||
| type : "qc", | |||
| status : "pending", // TODO: update with supervisor decision | |||
| reason : data.escalationLog?.reason, | |||
| recordDate : dayjsToDateTimeString(dayjs()), | |||
| handlerId : data.escalationLog?.handlerId, | |||
| } | |||
| console.log("Escalation Data for submission", escalationLog); | |||
| await postStockInLine({...qcData, escalationLog}); | |||
| } else { | |||
| await postStockInLine(qcData); | |||
| } | |||
| if (qcData.qcAccept) { | |||
| // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| // onOpenPutaway(); | |||
| closeHandler({}, "backdropClick"); | |||
| // setTabIndex(1); // Need to go Putaway tab? | |||
| } else { | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| msg("已更新來貨狀態"); | |||
| return ; | |||
| }, | |||
| [onOpenPutaway, formProps.formState.errors], | |||
| ); | |||
| const postStockInLine = useCallback(async (args: ModalFormInput) => { | |||
| const submitData = { | |||
| ...stockInLineInfo, ...args | |||
| } as StockInLineEntry & ModalFormInput; | |||
| console.log("Submitting", submitData); | |||
| const res = await updateStockInLine(submitData); | |||
| return res; | |||
| }, [stockInLineInfo]) | |||
| // Put away model | |||
| const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({}) | |||
| const [pafRowSelectionModel, setPafRowSelectionModel] = useState<GridRowSelectionModel>([]) | |||
| const pafSubmitDisable = useMemo(() => { | |||
| return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit) | |||
| }, [pafRowModesModel]) | |||
| // Putaway submission handler | |||
| const onSubmitPutaway = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| // Extract only putaway related fields | |||
| const putawayData = { | |||
| acceptQty: Number(data.acceptQty?? (stockInLineInfo?.demandQty?? (stockInLineInfo?.acceptedQty))), //TODO improve | |||
| warehouseId: data.warehouseId, | |||
| status: data.status, //TODO Fix it! | |||
| // ...data, | |||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| productionDate : arrayToDateString(data.productionDate, "input"), | |||
| expiryDate : arrayToDateString(data.expiryDate, "input"), | |||
| receiptDate : arrayToDateString(data.receiptDate, "input"), | |||
| // for putaway data | |||
| inventoryLotLines: data.putAwayLines?.filter((line) => line._isNew !== false) | |||
| // Add other putaway specific fields | |||
| } as ModalFormInput; | |||
| console.log("Putaway Data:", putawayData); | |||
| console.log("DEBUG",data.putAwayLines); | |||
| // if (data.putAwayLines!!.filter((line) => line._isNew !== false).length <= 0) { | |||
| // alert("請新增上架資料!"); | |||
| // return; | |||
| // } | |||
| if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve | |||
| alert("上架數量不正確!"); | |||
| return; | |||
| } | |||
| if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) { | |||
| alert(`上架數量不能大於 ${putawayData.acceptQty}!`); | |||
| return; | |||
| } | |||
| // Handle putaway submission logic here | |||
| const res = await postStockInLine(putawayData); | |||
| console.log("result ", res); | |||
| // Close modal after successful putaway | |||
| closeHandler({}, "backdropClick"); | |||
| }, | |||
| [closeHandler], | |||
| ); | |||
| // Print handler | |||
| const [isPrinting, setIsPrinting] = useState(false) | |||
| const handlePrint = useCallback(async () => { | |||
| // console.log("Print putaway documents"); | |||
| console.log("%c data", "background: white; color: red", formProps.watch("putAwayLines")); | |||
| // Handle print logic here | |||
| // window.print(); | |||
| // const postData = { stockInLineIds: [itemDetail.id]}; | |||
| // const response = await fetchPoQrcode(postData); | |||
| // if (response) { | |||
| // downloadFile(new Uint8Array(response.blobValue), response.filename) | |||
| // } | |||
| try { | |||
| setIsPrinting(() => true) | |||
| if ((formProps.watch("putAwayLines") ?? []).filter((line) => /[^0-9]/.test(String(line.printQty))).length > 0) { //TODO Improve | |||
| alert("列印數量不正確!"); | |||
| return; | |||
| } | |||
| // console.log(pafRowSelectionModel) | |||
| const printList = formProps.watch("putAwayLines")?.filter((line) => ((pafRowSelectionModel ?? []).some((model) => model === line.id))) ?? [] | |||
| // const printQty = printList.reduce((acc, cur) => acc + cur.printQty, 0) | |||
| // console.log(printQty) | |||
| const data: PrintQrCodeForSilRequest = { | |||
| stockInLineId: stockInLineInfo?.id ?? 0, | |||
| printerId: selectedPrinter.id, | |||
| printQty: printQty | |||
| } | |||
| const response = await printQrCodeForSil(data); | |||
| if (response) { | |||
| console.log(response) | |||
| } | |||
| } finally { | |||
| setIsPrinting(() => false) | |||
| } | |||
| // }, [pafRowSelectionModel, printQty, selectedPrinter]); | |||
| }, [stockInLineInfo?.id, pafRowSelectionModel, printQty, selectedPrinter]); | |||
| const acceptQty = formProps.watch("acceptedQty") | |||
| // const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => { | |||
| // const isPassed = qcItems.every((qc) => qc.qcPassed); | |||
| // console.log(isPassed) | |||
| // if (isPassed) { | |||
| // formProps.setValue("passingQty", acceptQty) | |||
| // } else { | |||
| // formProps.setValue("passingQty", 0) | |||
| // } | |||
| // return isPassed | |||
| // }, [acceptQty, formProps]) | |||
| const printQrcode = useCallback( | |||
| async () => { | |||
| setIsPrinting(true); | |||
| try { | |||
| const postData = { stockInLineIds: [stockInLineInfo?.id] }; | |||
| const response = await fetchPoQrcode(postData); | |||
| if (response) { | |||
| console.log(response); | |||
| downloadFile(new Uint8Array(response.blobValue), response.filename!); | |||
| } | |||
| } catch (e) { | |||
| console.log("%c Error downloading QR Code", "color:red", e); | |||
| } finally { | |||
| setIsPrinting(false); | |||
| } | |||
| }, | |||
| [stockInLineInfo], | |||
| ); | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <Box | |||
| sx={{ | |||
| ...style, | |||
| // padding: 2, | |||
| maxHeight: "90vh", | |||
| overflowY: "auto", | |||
| marginLeft: 3, | |||
| marginRight: 3, | |||
| // overflow: "hidden", | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| > | |||
| {(!isLoading && stockInLineInfo) ? (<> | |||
| <Box sx={{ position: 'sticky', top: 0, bgcolor: 'background.paper', | |||
| zIndex: 5, borderBottom: 2, borderColor: 'divider', width: "100%"}}> | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| sx={{pl: 2, pr: 2, pt: 2}} | |||
| > | |||
| <Tab label={ | |||
| showPutaway ? t("dn and qc info") : t("qc processing") | |||
| } iconPosition="end" /> | |||
| {showPutaway && <Tab label={t("putaway processing")} iconPosition="end" />} | |||
| </Tabs> | |||
| </Box> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| sx={{padding: 2}} | |||
| > | |||
| <Grid item xs={12}> | |||
| {tabIndex === 0 && | |||
| <Box> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Delivery Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| {stockInLineInfo.jobOrderId ? ( | |||
| <FgStockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} /> | |||
| ) : ( | |||
| <StockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} /> | |||
| ) | |||
| } | |||
| {skipQc === false && ( | |||
| <QcComponent | |||
| itemDetail={stockInLineInfo} | |||
| disabled={viewOnly || showPutaway} | |||
| />) | |||
| } | |||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{pt:2}}> | |||
| {(!viewOnly && !showPutaway) && (<Button | |||
| id="Submit" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)} | |||
| > | |||
| {skipQc ? t("confirm") : t("confirm qc result")} | |||
| </Button>)} | |||
| </Stack> | |||
| </Box> | |||
| } | |||
| {tabIndex === 1 && | |||
| <Box> | |||
| <PutAwayForm | |||
| itemDetail={stockInLineInfo} | |||
| warehouse={warehouse!} | |||
| disabled={viewOnly} | |||
| setRowModesModel={setPafRowModesModel} | |||
| setRowSelectionModel={setPafRowSelectionModel} | |||
| /> | |||
| </Box> | |||
| } | |||
| </Grid> | |||
| </Grid> | |||
| {tabIndex == 1 && ( | |||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo} | |||
| defaultValue={selectedPrinter} | |||
| onChange={(event, value) => { | |||
| setSelectedPrinter(value) | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| variant="outlined" | |||
| label={t("Printer")} | |||
| sx={{ width: 300}} | |||
| /> | |||
| )} | |||
| /> | |||
| <TextField | |||
| variant="outlined" | |||
| label={t("Print Qty")} | |||
| defaultValue={printQty} | |||
| onChange={(event) => { | |||
| event.target.value = event.target.value.replace(/[^0-9]/g, '') | |||
| setPrintQty(Number(event.target.value)) | |||
| }} | |||
| sx={{ width: 300}} | |||
| /> | |||
| <Button | |||
| id="printButton" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={handlePrint} | |||
| disabled={isPrinting || printerCombo.length <= 0 || pafSubmitDisable} | |||
| > | |||
| {isPrinting ? t("Printing") : t("print")} | |||
| </Button> | |||
| <Button | |||
| id="demoPrint" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={printQrcode} | |||
| disabled={isPrinting} | |||
| > | |||
| {isPrinting ? t("downloading") : t("download Qr Code")} | |||
| </Button> | |||
| </Stack> | |||
| )} | |||
| </>) : <LoadingComponent/>} | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default QcStockInModal; | |||