| @@ -5,8 +5,9 @@ import { revalidateTag } from "next/cache"; | |||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| //import { serverFetchJson } from "@/app/utils/fetchUtil"; | //import { serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { serverFetchJson } from "../../utils/fetchUtil"; | import { serverFetchJson } from "../../utils/fetchUtil"; | ||||
| import { QcItemWithChecks } from "."; | |||||
| import { QcCategory, QcItemWithChecks } from "."; | |||||
| // DEPRECIATED | |||||
| export interface QcResult { | export interface QcResult { | ||||
| id: number; | id: number; | ||||
| qcItemId: 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) => { | export const fetchQcResult = cache(async (id: number) => { | ||||
| return serverFetchJson<QcResult[]>(`${BASE_API_URL}/qcResult/${id}`, { | return serverFetchJson<QcResult[]>(`${BASE_API_URL}/qcResult/${id}`, { | ||||
| next: { tags: ["qc"] }, | next: { tags: ["qc"] }, | ||||
| @@ -15,23 +15,50 @@ export interface QcItemWithChecks { | |||||
| description: string | undefined; | 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; | id?: number; | ||||
| qcItemId: number; | qcItemId: number; | ||||
| qcPassed?: boolean; | qcPassed?: boolean; | ||||
| failQty?: number; | failQty?: number; | ||||
| remarks?: string; | remarks?: string; | ||||
| escalationLogId?: number; | 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 () => { | export const fetchQcItemCheckList = cache(async () => { | ||||
| @@ -6,7 +6,7 @@ import { serverFetchJson } from "../../utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "../../../config/api"; | import { BASE_API_URL } from "../../../config/api"; | ||||
| import { Uom } from "../settings/uom"; | import { Uom } from "../settings/uom"; | ||||
| import { RecordsRes } from "../utils"; | import { RecordsRes } from "../utils"; | ||||
| import { QcResult } from "../qc"; | |||||
| import { QcFormInput, QcResult } from "../qc"; | |||||
| import { EscalationResult } from "../escalation"; | import { EscalationResult } from "../escalation"; | ||||
| export enum StockInStatus { | export enum StockInStatus { | ||||
| @@ -156,14 +156,9 @@ export interface PutAwayInput { | |||||
| warehouseId: number; | warehouseId: number; | ||||
| putAwayLines: PutAwayLine[] | putAwayLines: PutAwayLine[] | ||||
| } | } | ||||
| export interface QcInput { | |||||
| acceptQty: number; | |||||
| qcAccept: boolean; | |||||
| qcDecision?: number; | |||||
| qcResult: QcResult[]; | |||||
| } | |||||
| export type ModalFormInput = Partial< | export type ModalFormInput = Partial< | ||||
| QcInput & StockInInput & PutAwayInput | |||||
| QcFormInput & StockInInput & PutAwayInput | |||||
| > & { | > & { | ||||
| escalationLog? : Partial<EscalationInput> | 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 [selectedDate, setSelectedDate] = useState<string>("today"); | const [selectedDate, setSelectedDate] = useState<string>("today"); | ||||
| const loadData = async (dateValue: string) => { | |||||
| const loadSummaries = useCallback(async () => { | |||||
| setIsLoadingSummary(true); | setIsLoadingSummary(true); | ||||
| try { | 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([ | 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); | setSummary2F(s2); | ||||
| setSummary4F(s4); | setSummary4F(s4); | ||||
| } catch (e) { | |||||
| console.error("load summaries failed:", e); | |||||
| } catch (error) { | |||||
| console.error("Error loading summaries:", error); | |||||
| } finally { | } finally { | ||||
| setIsLoadingSummary(false); | setIsLoadingSummary(false); | ||||
| } | } | ||||
| }; | |||||
| }, [selectedDate]); | |||||
| // 初始化 | // 初始化 | ||||
| useEffect(() => { | useEffect(() => { | ||||
| loadData("today"); | |||||
| }, []); | |||||
| loadSummaries(); | |||||
| }, [loadSummaries]); | |||||
| const handleAssignByLane = useCallback(async ( | const handleAssignByLane = useCallback(async ( | ||||
| storeId: string, | storeId: string, | ||||
| @@ -72,7 +73,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||||
| if (res.code === "SUCCESS") { | if (res.code === "SUCCESS") { | ||||
| console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | console.log("✅ Successfully assigned pick order from lane", truckLanceCode); | ||||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | ||||
| loadData(selectedDate); // 刷新按钮状态 | |||||
| loadSummaries(); // 刷新按钮状态 | |||||
| onPickOrderAssigned?.(); | onPickOrderAssigned?.(); | ||||
| } else if (res.code === "USER_BUSY") { | } else if (res.code === "USER_BUSY") { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| @@ -141,9 +142,10 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) => | |||||
| onChange={(e) => { { | onChange={(e) => { { | ||||
| setSelectedDate(e.target.value); | setSelectedDate(e.target.value); | ||||
| loadData(e.target.value); | |||||
| loadSummaries(); | |||||
| }}} | }}} | ||||
| > | > | ||||
| <MenuItem value="today"> | <MenuItem value="today"> | ||||
| {t("Today")} ({getDateLabel(0)}) | {t("Today")} ({getDateLabel(0)}) | ||||
| </MenuItem> | </MenuItem> | ||||
| @@ -16,7 +16,7 @@ import { Button, Stack } from "@mui/material"; | |||||
| import { BomCombo } from "@/app/api/bom"; | import { BomCombo } from "@/app/api/bom"; | ||||
| import JoCreateFormModal from "./JoCreateFormModal"; | import JoCreateFormModal from "./JoCreateFormModal"; | ||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||
| import QcStockInModal from "../PoDetail/QcStockInModal"; | |||||
| import QcStockInModal from "../Qc/QcStockInModal"; | |||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { createStockInLine } from "@/app/api/stockIn/actions"; | 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 { downloadFile } from "@/app/utils/commonUtil"; | ||||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | import { fetchPoQrcode } from "@/app/api/pdf/actions"; | ||||
| import { fetchQcResult } from "@/app/api/qc/actions"; | import { fetchQcResult } from "@/app/api/qc/actions"; | ||||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||||
| import DoDisturbIcon from "@mui/icons-material/DoDisturb"; | import DoDisturbIcon from "@mui/icons-material/DoDisturb"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| // import { SessionWithTokens } from "src/config/authConfig"; | // import { SessionWithTokens } from "src/config/authConfig"; | ||||
| import QcStockInModal from "./QcStockInModal"; | |||||
| import QcStockInModal from "../Qc/QcStockInModal"; | |||||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| @@ -256,31 +255,31 @@ function PoInputGrid({ | |||||
| return await fetchQcResult(stockInLineId as number); | 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 [newOpen, setNewOpen] = useState(false); | ||||
| const stockInLineId = searchParams.get("stockInLineId"); | const stockInLineId = searchParams.get("stockInLineId"); | ||||
| @@ -326,7 +325,7 @@ const closeNewModal = useCallback(() => { | |||||
| // setTimeout(() => { | // setTimeout(() => { | ||||
| // }, 200); | // }, 200); | ||||
| }, | }, | ||||
| [fetchQcDefaultValue, openNewModal, pathname, router, searchParams] | |||||
| [openNewModal, pathname, router, searchParams] | |||||
| ); | ); | ||||
| // Open modal if `stockInLineId` exists in the URL | // 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(() => | 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 { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
| import { dummyPutAwayLine } from "./dummyQcTemplate"; | |||||
| import { dummyPutAwayLine } from "../Qc/dummyQcTemplate"; | |||||
| import { GridRowModesModel } from "@mui/x-data-grid"; | import { GridRowModesModel } from "@mui/x-data-grid"; | ||||
| dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
| @@ -37,7 +37,7 @@ import { | |||||
| GridApiCommunity, | GridApiCommunity, | ||||
| GridSlotsComponentsProps, | GridSlotsComponentsProps, | ||||
| } from "@mui/x-data-grid/internals"; | } from "@mui/x-data-grid/internals"; | ||||
| import { dummyQCData } from "./dummyQcTemplate"; | |||||
| import { dummyQCData } from "../Qc/dummyQcTemplate"; | |||||
| // T == CreatexxxInputs map of the form's fields | // T == CreatexxxInputs map of the form's fields | ||||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | ||||
| // E == error | // E == error | ||||
| @@ -50,7 +50,7 @@ type EntryError = | |||||
| type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | ||||
| // fetchQcItemCheck | // fetchQcItemCheck | ||||
| const QcForm: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| const QcFormOld: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| const { | const { | ||||
| @@ -313,4 +313,4 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default QcForm; | |||||
| export default QcFormOld; | |||||
| @@ -1,24 +1,9 @@ | |||||
| "use client"; | "use client"; | ||||
| import { QcResult, QCInput } from "@/app/api/stockIn/actions"; | |||||
| import { | 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"; | } from "@mui/material"; | ||||
| import { useFormContext, Controller, FieldPath } from "react-hook-form"; | import { useFormContext, Controller, FieldPath } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| @@ -36,31 +21,32 @@ import { | |||||
| } from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
| import InputDataGrid from "../InputDataGrid"; | import InputDataGrid from "../InputDataGrid"; | ||||
| import { TableRow } from "../InputDataGrid/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 { GridEditInputCell } from "@mui/x-data-grid"; | ||||
| import { ModalFormInput, StockInLine } from "@/app/api/stockIn"; | 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 axios from "@/app/(main)/axios/axiosInstance"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | 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, | import { dummyEscalationHistory, | ||||
| dummyQcData_A1, dummyQcData_E1, dummyQcData_E2, | dummyQcData_A1, dummyQcData_E1, dummyQcData_E2, | ||||
| dummyQcHeader_A1, dummyQcHeader_E1, dummyQcHeader_E2 } from "./dummyQcTemplate"; | 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 { PanoramaSharp } from "@mui/icons-material"; | ||||
| import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | ||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| import { EscalationCombo } from "@/app/api/user"; | import { EscalationCombo } from "@/app/api/user"; | ||||
| import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; | |||||
| import CollapsibleCard from "../CollapsibleCard/CollapsibleCard"; | import CollapsibleCard from "../CollapsibleCard/CollapsibleCard"; | ||||
| import LoadingComponent from "../General/LoadingComponent"; | import LoadingComponent from "../General/LoadingComponent"; | ||||
| import QcForm from "./QcForm"; | |||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | |||||
| itemDetail: QcInput; | |||||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | ||||
| // qc: QcItemWithChecks[]; | // qc: QcItemWithChecks[]; | ||||
| disabled: boolean; | disabled: boolean; | ||||
| @@ -74,7 +60,7 @@ type EntryError = | |||||
| } | } | ||||
| | undefined; | | undefined; | ||||
| type QcRow = TableRow<Partial<QcData>, EntryError>; | |||||
| type QcRow = TableRow<Partial<QcResult>, EntryError>; | |||||
| // fetchQcItemCheck | // fetchQcItemCheck | ||||
| const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | ||||
| const { t } = useTranslation("purchaseOrder"); | const { t } = useTranslation("purchaseOrder"); | ||||
| @@ -90,7 +76,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| resetField, | resetField, | ||||
| setError, | setError, | ||||
| clearErrors, | clearErrors, | ||||
| } = useFormContext<QCInput>(); | |||||
| } = useFormContext<QcFormInput>(); | |||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>(); | const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>(); | ||||
| @@ -98,6 +84,8 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| const qcAccept = watch("qcAccept"); | const qcAccept = watch("qcAccept"); | ||||
| const qcDecision = watch("qcDecision"); //WIP | const qcDecision = watch("qcDecision"); //WIP | ||||
| // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); | // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); | ||||
| const [qcCategory, setQcCategory] = useState<QcCategory>(); | |||||
| const qcRecord = useMemo(() => { // Need testing | const qcRecord = useMemo(() => { // Need testing | ||||
| const value = watch('qcResult'); //console.log("%c QC update!", "color:green", value); | 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')]); | }, [watch('qcResult')]); | ||||
| const [qcHistory, setQcHistory] = useState<QcResult[]>([]); | const [qcHistory, setQcHistory] = useState<QcResult[]>([]); | ||||
| const [qcResult, setQcResult] = 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 [qcAccept, setQcAccept] = useState(true); | ||||
| // const [qcItems, setQcItems] = useState(dummyQCData) | // 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"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
| (_e, newValue) => { | (_e, newValue) => { | ||||
| setTabIndex(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 // | // 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) | // console.log("Checking if " + message) | ||||
| if (condition) { setError(field, { message: message}); return false; } | if (condition) { setError(field, { message: message}); return false; } | ||||
| else { clearErrors(field); return true; } | else { clearErrors(field); return true; } | ||||
| @@ -166,8 +154,8 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| } else | } else | ||||
| console.log("%c Validated accQty:", "color:yellow", accQty); | console.log("%c Validated accQty:", "color:yellow", accQty); | ||||
| } | } | ||||
| },[setError, qcDecision, accQty, itemDetail]) | |||||
| },[setError, qcDecision, accQty, itemDetail]) | |||||
| useEffect(() => { // W I P // ----- | useEffect(() => { // W I P // ----- | ||||
| if (qcDecision == 1) { | if (qcDecision == 1) { | ||||
| if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ | 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 | // Set initial value for acceptQty | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (itemDetail?.demandQty > 0) { //!== undefined) { | 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 { | } else { | ||||
| setValue("acceptQty", itemDetail?.acceptedQty); | setValue("acceptQty", itemDetail?.acceptedQty); | ||||
| } | } | ||||
| }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | ||||
| // Fetch Qc Data | |||||
| useEffect(() => { | 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)); | const filteredQcResult = qcRecord.filter((qc) => !isExist(qc.escalationLogId)); | ||||
| console.log("%c QC Result loaded:", "color:green", filteredQcResult); | |||||
| setQcResult(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 [openCollapse, setOpenCollapse] = useState(false) | ||||
| const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | ||||
| @@ -435,11 +368,6 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| // }, [setValue]); | // }, [setValue]); | ||||
| useEffect(() => { | |||||
| // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||||
| }, [itemDetail]); | |||||
| const setDefaultQcDecision = (status : string | undefined) => { | const setDefaultQcDecision = (status : string | undefined) => { | ||||
| const param = status?.toLowerCase(); | const param = status?.toLowerCase(); | ||||
| if (param !== undefined && param !== null) { | if (param !== undefined && param !== null) { | ||||
| @@ -473,37 +401,46 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| } else { return 60} | } 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 ( | return ( | ||||
| <> | <> | ||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | <Grid container justifyContent="flex-start" alignItems="flex-start"> | ||||
| {itemDetail ? ( | |||||
| {(qcRecord.length > 0) ? ( | |||||
| // {(qcRecord.length > 0 && qcCategory) ? ( | |||||
| <Grid | <Grid | ||||
| container | container | ||||
| justifyContent="flex-start" | justifyContent="flex-start" | ||||
| @@ -518,41 +455,23 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| variant="scrollable" | variant="scrollable" | ||||
| > | > | ||||
| <Tab label={t("QC Info")} iconPosition="end" /> | <Tab label={t("QC Info")} iconPosition="end" /> | ||||
| {(itemDetail.escResult && itemDetail.escResult?.length > 0) && | |||||
| {(escResult && escResult?.length > 0) && | |||||
| (<Tab label={t("Escalation History")} iconPosition="end" />)} | (<Tab label={t("Escalation History")} iconPosition="end" />)} | ||||
| </Tabs> | </Tabs> | ||||
| </Grid> | </Grid> | ||||
| {tabIndex == 0 && ( | {tabIndex == 0 && ( | ||||
| <> | <> | ||||
| <Grid item xs={12}> | <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> | {/* <QcDataGrid<ModalFormInput, QcData, EntryError> | ||||
| apiRef={apiRef} | apiRef={apiRef} | ||||
| columns={qcColumns} | columns={qcColumns} | ||||
| _formKey="qcResult" | _formKey="qcResult" | ||||
| validateRow={validation} | validateRow={validation} | ||||
| /> */} | /> */} | ||||
| <StyledDataGrid | |||||
| columns={qcColumns} | |||||
| <QcForm | |||||
| rows={qcResult} | rows={qcResult} | ||||
| // rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||||
| // rows={disabled? qcResult:qcItems} | |||||
| // autoHeight | |||||
| sortModel={[]} | |||||
| // getRowHeight={getRowHeight} | |||||
| getRowHeight={() => 'auto'} | |||||
| getRowId={getRowId} | |||||
| disabled={disabled} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| </> | </> | ||||
| @@ -571,26 +490,12 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| </Typography> | </Typography> | ||||
| </Grid> */} | </Grid> */} | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <EscalationLogTable type="qc" items={itemDetail.escResult || []}/> | |||||
| <EscalationLogTable type="qc" items={escResult || []}/> | |||||
| <CollapsibleCard title={t("QC Record")}> | <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={qcHistory} | ||||
| // rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||||
| // rows={disabled? qcResult:qcItems} | |||||
| autoHeight | |||||
| sortModel={[]} | |||||
| /> | /> | ||||
| </CollapsibleCard> | </CollapsibleCard> | ||||
| </Grid> | </Grid> | ||||
| @@ -712,7 +617,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| <FormControlLabel disabled={disabled} | <FormControlLabel disabled={disabled} | ||||
| value="3" control={<Radio />} | value="3" control={<Radio />} | ||||
| sx={{"& .Mui-checked": {color: "blue"}}} | sx={{"& .Mui-checked": {color: "blue"}}} | ||||
| label="上報品檢結果" /> | |||||
| label="暫時存放到置物區,並等待品檢結果" /> | |||||
| </>)} | </>)} | ||||
| </RadioGroup> | </RadioGroup> | ||||
| </> | </> | ||||
| @@ -730,18 +635,6 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||||
| setIsCollapsed={setIsCollapsed} | setIsCollapsed={setIsCollapsed} | ||||
| /> | /> | ||||
| </Grid>)} | </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> | </Grid> | ||||
| ) : <LoadingComponent/>} | ) : <LoadingComponent/>} | ||||
| </Grid> | </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; | |||||