diff --git a/src/app/api/qc/actions.ts b/src/app/api/qc/actions.ts index f7b19c7..10fd816 100644 --- a/src/app/api/qc/actions.ts +++ b/src/app/api/qc/actions.ts @@ -5,8 +5,9 @@ import { revalidateTag } from "next/cache"; import { cache } from "react"; //import { serverFetchJson } from "@/app/utils/fetchUtil"; import { serverFetchJson } from "../../utils/fetchUtil"; -import { QcItemWithChecks } from "."; +import { QcCategory, QcItemWithChecks } from "."; +// DEPRECIATED export interface QcResult { id: number; qcItemId: number; @@ -43,6 +44,16 @@ export const fetchQcItemCheck = cache(async (itemId?: number) => { }); }); +export const fetchQcCategory = cache(async (itemId: number, type: string = "IQC") => { + const params = new URLSearchParams({ + itemId: itemId.toString(), + type, + }).toString(); + return serverFetchJson(`${BASE_API_URL}/qcCategories/items?${params}`, { + next: { tags: ["qc"] }, + }); +}); + export const fetchQcResult = cache(async (id: number) => { return serverFetchJson(`${BASE_API_URL}/qcResult/${id}`, { next: { tags: ["qc"] }, diff --git a/src/app/api/qc/index.ts b/src/app/api/qc/index.ts index d178c2a..b862d63 100644 --- a/src/app/api/qc/index.ts +++ b/src/app/api/qc/index.ts @@ -15,23 +15,50 @@ export interface QcItemWithChecks { description: string | undefined; } -export interface QcResult{ +export interface QcCategory { + id: number, + code?: string, + name?: string, + description?: string, + qcItems: QcData[], +} +export interface QcData { + id?: number, + qcItemId: number, + code?: string, + name?: string, + order?: number, + description?: string, + // qcPassed: boolean | undefined + // failQty: number | undefined + // remarks: string | undefined +} +export interface QcResult extends QcData{ id?: number; qcItemId: number; qcPassed?: boolean; failQty?: number; remarks?: string; escalationLogId?: number; + stockInLineId?: number; + stockOutLineId?: number; } -export interface QcData { - id?: number, - qcItemId: number, - code: string, - name: string, - description: string, - qcPassed: boolean | undefined - failQty: number | undefined - remarks: string | undefined +export interface QcInput { + id: number; + itemId: number; + acceptedQty: number; + demandQty: number; + status?: string; + + jobOrderId: number; + purchaseOrderId?: number; + purchaseOrderLineId: number; +} +export interface QcFormInput { + acceptQty: number; + qcAccept: boolean; + qcDecision?: number; + qcResult: QcResult[]; } export const fetchQcItemCheckList = cache(async () => { diff --git a/src/app/api/stockIn/index.ts b/src/app/api/stockIn/index.ts index aacd62b..17dda00 100644 --- a/src/app/api/stockIn/index.ts +++ b/src/app/api/stockIn/index.ts @@ -6,7 +6,7 @@ import { serverFetchJson } from "../../utils/fetchUtil"; import { BASE_API_URL } from "../../../config/api"; import { Uom } from "../settings/uom"; import { RecordsRes } from "../utils"; -import { QcResult } from "../qc"; +import { QcFormInput, QcResult } from "../qc"; import { EscalationResult } from "../escalation"; export enum StockInStatus { @@ -156,14 +156,9 @@ export interface PutAwayInput { warehouseId: number; putAwayLines: PutAwayLine[] } -export interface QcInput { - acceptQty: number; - qcAccept: boolean; - qcDecision?: number; - qcResult: QcResult[]; -} + export type ModalFormInput = Partial< - QcInput & StockInInput & PutAwayInput + QcFormInput & StockInInput & PutAwayInput > & { escalationLog? : Partial }; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx index 56a0afa..1961041 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx @@ -25,35 +25,36 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned }) => //const [selectedDate, setSelectedDate] = useState("today"); const [selectedDate, setSelectedDate] = useState("today"); - const loadData = async (dateValue: string) => { + const loadSummaries = useCallback(async () => { setIsLoadingSummary(true); try { - let dateOffset = 0; - if (dateValue === "tomorrow") dateOffset = 1; - else if (dateValue === "dayAfterTomorrow") dateOffset = 2; - - const requiredDate = dayjs().add(dateOffset, "day").format("YYYY-MM-DD"); - console.log("🔄 requiredDate:", requiredDate); + // Convert selectedDate to the format needed + let dateParam: string | undefined; + if (selectedDate === "today") { + dateParam = dayjs().format('YYYY-MM-DD'); + } else if (selectedDate === "tomorrow") { + dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD'); + } else if (selectedDate === "dayAfterTomorrow") { + dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD'); + } + const [s2, s4] = await Promise.all([ - fetchStoreLaneSummary("2/F", requiredDate), - fetchStoreLaneSummary("4/F", requiredDate), + fetchStoreLaneSummary("2/F", dateParam), + fetchStoreLaneSummary("4/F", dateParam) ]); - console.log("🔄 s2:", s2); - console.log("🔄 s4:", s4); - setSummary2F(s2); setSummary4F(s4); - } catch (e) { - console.error("load summaries failed:", e); + } catch (error) { + console.error("Error loading summaries:", error); } finally { setIsLoadingSummary(false); } - }; + }, [selectedDate]); // 初始化 useEffect(() => { - loadData("today"); - }, []); + loadSummaries(); + }, [loadSummaries]); const handleAssignByLane = useCallback(async ( storeId: string, @@ -72,7 +73,7 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned }) => if (res.code === "SUCCESS") { console.log("✅ Successfully assigned pick order from lane", truckLanceCode); window.dispatchEvent(new CustomEvent('pickOrderAssigned')); - loadData(selectedDate); // 刷新按钮状态 + loadSummaries(); // 刷新按钮状态 onPickOrderAssigned?.(); } else if (res.code === "USER_BUSY") { Swal.fire({ @@ -141,9 +142,10 @@ const FinishedGoodFloorLanePanel: React.FC = ({ onPickOrderAssigned }) => onChange={(e) => { { setSelectedDate(e.target.value); - loadData(e.target.value); + loadSummaries(); }}} > + {t("Today")} ({getDateLabel(0)}) diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index bb6d596..b05ee0d 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -16,7 +16,7 @@ import { Button, Stack } from "@mui/material"; import { BomCombo } from "@/app/api/bom"; import JoCreateFormModal from "./JoCreateFormModal"; import AddIcon from '@mui/icons-material/Add'; -import QcStockInModal from "../PoDetail/QcStockInModal"; +import QcStockInModal from "../Qc/QcStockInModal"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { createStockInLine } from "@/app/api/stockIn/actions"; diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index 4918f4e..96c0274 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -57,11 +57,10 @@ import QrCodeIcon from "@mui/icons-material/QrCode"; import { downloadFile } from "@/app/utils/commonUtil"; import { fetchPoQrcode } from "@/app/api/pdf/actions"; import { fetchQcResult } from "@/app/api/qc/actions"; -import PoQcStockInModal from "./PoQcStockInModal"; import DoDisturbIcon from "@mui/icons-material/DoDisturb"; import { useSession } from "next-auth/react"; // import { SessionWithTokens } from "src/config/authConfig"; -import QcStockInModal from "./QcStockInModal"; +import QcStockInModal from "../Qc/QcStockInModal"; import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; import { PrinterCombo } from "@/app/api/settings/printer"; import { EscalationResult } from "@/app/api/escalation"; @@ -256,31 +255,31 @@ function PoInputGrid({ return await fetchQcResult(stockInLineId as number); }, []); - const handleQC = useCallback( // UNUSED NOW! - (id: GridRowId, params: any) => async () => { - setBtnIsLoading(true); - setRowModesModel((prev) => ({ - ...prev, - [id]: { mode: GridRowModes.View }, - })); - const qcResult = await fetchQcDefaultValue(id); - // console.log(params.row); - console.log("Fetched QC Result:", qcResult); + // const handleQC = useCallback( // UNUSED NOW! + // (id: GridRowId, params: any) => async () => { + // setBtnIsLoading(true); + // setRowModesModel((prev) => ({ + // ...prev, + // [id]: { mode: GridRowModes.View }, + // })); + // const qcResult = await fetchQcDefaultValue(id); + // // console.log(params.row); + // console.log("Fetched QC Result:", qcResult); - setModalInfo({ - ...params.row, - qcResult: qcResult, - }); - // set default values - setTimeout(() => { - // open qc modal - console.log("delayed"); - openQcModal(); - setBtnIsLoading(false); - }, 200); - }, - [fetchQcDefaultValue, openQcModal], - ); + // setModalInfo({ + // ...params.row, + // qcResult: qcResult, + // }); + // // set default values + // setTimeout(() => { + // // open qc modal + // console.log("delayed"); + // openQcModal(); + // setBtnIsLoading(false); + // }, 200); + // }, + // [fetchQcDefaultValue, openQcModal], + // ); const [newOpen, setNewOpen] = useState(false); const stockInLineId = searchParams.get("stockInLineId"); @@ -326,7 +325,7 @@ const closeNewModal = useCallback(() => { // setTimeout(() => { // }, 200); }, - [fetchQcDefaultValue, openNewModal, pathname, router, searchParams] + [openNewModal, pathname, router, searchParams] ); // Open modal if `stockInLineId` exists in the URL @@ -793,7 +792,7 @@ const closeNewModal = useCallback(() => { }, }, ], - [t, handleStart, handleQC, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], + [t, handleStart, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], ); const unsortableColumns = useMemo(() => diff --git a/src/components/PoDetail/PoQcStockInModal.tsx b/src/components/PoDetail/PoQcStockInModal.tsx deleted file mode 100644 index 2cb8254..0000000 --- a/src/components/PoDetail/PoQcStockInModal.tsx +++ /dev/null @@ -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 { - // setRows: Dispatch>; - setEntries?: Dispatch>; - setStockInLine?: Dispatch>; - 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 = ({ - 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({ - 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>( - (...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>( - 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 ( - <> - - - - {itemDetail !== undefined && type === "qc" && ( - - )} - {itemDetail !== undefined && type === "escalation" && ( - - )} - {itemDetail !== undefined && type === "stockIn" && ( - - )} - {itemDetail !== undefined && type === "putaway" && ( - - )} - {itemDetail !== undefined && type === "reject" && ( - - )} - - {renderSubmitButton ? ( - - ) : undefined} - {itemDetail !== undefined && type === "putaway" && ( - - )} - - - - - - ); -}; -export default PoQcStockInModal; diff --git a/src/components/PoDetail/PutAwayForm.tsx b/src/components/PoDetail/PutAwayForm.tsx index bdc8c4c..58c0ff0 100644 --- a/src/components/PoDetail/PutAwayForm.tsx +++ b/src/components/PoDetail/PutAwayForm.tsx @@ -53,7 +53,7 @@ import { QrCodeInfo } from "@/app/api/qrcode"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; -import { dummyPutAwayLine } from "./dummyQcTemplate"; +import { dummyPutAwayLine } from "../Qc/dummyQcTemplate"; import { GridRowModesModel } from "@mui/x-data-grid"; dayjs.extend(arraySupport); diff --git a/src/components/PoDetail/QCDatagrid.tsx b/src/components/PoDetail/QCDatagrid.tsx index b9947db..15af718 100644 --- a/src/components/PoDetail/QCDatagrid.tsx +++ b/src/components/PoDetail/QCDatagrid.tsx @@ -37,7 +37,7 @@ import { GridApiCommunity, GridSlotsComponentsProps, } from "@mui/x-data-grid/internals"; -import { dummyQCData } from "./dummyQcTemplate"; +import { dummyQCData } from "../Qc/dummyQcTemplate"; // T == CreatexxxInputs map of the form's fields // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc // E == error diff --git a/src/components/PoDetail/QcForm.tsx b/src/components/PoDetail/QcFormOld.tsx similarity index 98% rename from src/components/PoDetail/QcForm.tsx rename to src/components/PoDetail/QcFormOld.tsx index cfc4b77..c290586 100644 --- a/src/components/PoDetail/QcForm.tsx +++ b/src/components/PoDetail/QcFormOld.tsx @@ -50,7 +50,7 @@ type EntryError = type PoQcRow = TableRow, EntryError>; // fetchQcItemCheck -const QcForm: React.FC = ({ qc, itemDetail, disabled }) => { +const QcFormOld: React.FC = ({ qc, itemDetail, disabled }) => { const { t } = useTranslation("purchaseOrder"); const apiRef = useGridApiRef(); const { @@ -313,4 +313,4 @@ const QcForm: React.FC = ({ qc, itemDetail, disabled }) => { ); }; -export default QcForm; +export default QcFormOld; diff --git a/src/components/PoDetail/QcComponent.tsx b/src/components/Qc/QcComponent.tsx similarity index 60% rename from src/components/PoDetail/QcComponent.tsx rename to src/components/Qc/QcComponent.tsx index 1a804d9..f20a606 100644 --- a/src/components/PoDetail/QcComponent.tsx +++ b/src/components/Qc/QcComponent.tsx @@ -1,24 +1,9 @@ "use client"; -import { QcResult, QCInput } from "@/app/api/stockIn/actions"; import { - Box, - Card, - CardContent, - Checkbox, - Collapse, - FormControl, - FormControlLabel, - Grid, - Radio, - RadioGroup, - Stack, - Tab, - Tabs, - TabsProps, - TextField, - Tooltip, - Typography, + Box, Card, CardContent, Checkbox, Collapse, FormControl, + FormControlLabel, Grid, Radio, RadioGroup, Stack, Tab, + Tabs, TabsProps, TextField, Tooltip, Typography, } from "@mui/material"; import { useFormContext, Controller, FieldPath } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -36,31 +21,32 @@ import { } from "@mui/x-data-grid"; import InputDataGrid from "../InputDataGrid"; import { TableRow } from "../InputDataGrid/InputDataGrid"; -import TwoLineCell from "./TwoLineCell"; -import QcSelect from "./QcSelect"; +import TwoLineCell from "../PoDetail/TwoLineCell"; +import QcSelect from "../PoDetail/QcSelect"; import { GridEditInputCell } from "@mui/x-data-grid"; import { ModalFormInput, StockInLine } from "@/app/api/stockIn"; -import { stockInLineStatusMap } from "@/app/utils/formatUtil"; -import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; -import { QcItemWithChecks, QcData } from "@/app/api/qc"; +import { fetchQcCategory, fetchQcResult } from "@/app/api/qc/actions"; +import { QcCategory, QcData, QcInput, QcFormInput, QcResult } from "@/app/api/qc"; import axios from "@/app/(main)/axios/axiosInstance"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import axiosInstance from "@/app/(main)/axios/axiosInstance"; -import EscalationComponent from "./EscalationComponent"; -import QcDataGrid from "./QCDatagrid"; +import EscalationComponent from "../PoDetail/EscalationComponent"; +import QcDataGrid from "../PoDetail/QCDatagrid"; import { dummyEscalationHistory, dummyQcData_A1, dummyQcData_E1, dummyQcData_E2, dummyQcHeader_A1, dummyQcHeader_E1, dummyQcHeader_E2 } from "./dummyQcTemplate"; -import { escape, isNull, min } from "lodash"; +import { escape, isNull, min, template } from "lodash"; import { PanoramaSharp } from "@mui/icons-material"; import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; import { EscalationResult } from "@/app/api/escalation"; import { EscalationCombo } from "@/app/api/user"; +import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; import CollapsibleCard from "../CollapsibleCard/CollapsibleCard"; import LoadingComponent from "../General/LoadingComponent"; +import QcForm from "./QcForm"; interface Props { - itemDetail: StockInLine; + itemDetail: QcInput; // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; // qc: QcItemWithChecks[]; disabled: boolean; @@ -74,7 +60,7 @@ type EntryError = } | undefined; -type QcRow = TableRow, EntryError>; +type QcRow = TableRow, EntryError>; // fetchQcItemCheck const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { const { t } = useTranslation("purchaseOrder"); @@ -90,7 +76,7 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { resetField, setError, clearErrors, - } = useFormContext(); + } = useFormContext(); const [tabIndex, setTabIndex] = useState(0); const [rowSelectionModel, setRowSelectionModel] = useState(); @@ -98,6 +84,8 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { const qcAccept = watch("qcAccept"); const qcDecision = watch("qcDecision"); //WIP // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); + + const [qcCategory, setQcCategory] = useState(); const qcRecord = useMemo(() => { // Need testing const value = watch('qcResult'); //console.log("%c QC update!", "color:green", value); @@ -105,30 +93,19 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { }, [watch('qcResult')]); const [qcHistory, setQcHistory] = useState([]); const [qcResult, setQcResult] = useState([]); - const [newQcData, setNewQcData] = useState([]); - - const detailMode = useMemo(() => { - const isDetailMode = itemDetail.status == "escalated" || isNaN(itemDetail.jobOrderId); - return isDetailMode; - }, [itemDetail]); + const [escResult, setEscResult] = useState([]); // const [qcAccept, setQcAccept] = useState(true); // const [qcItems, setQcItems] = useState(dummyQCData) - const column = useMemo( - () => [ - { - 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>( (_e, newValue) => { setTabIndex(newValue); @@ -136,12 +113,23 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { [], ); - const isExist = (data : string | number | undefined) => { - return (data !== null && data !== undefined); - } + const qcType = useMemo(() => { + if (itemDetail) { + const d = itemDetail; + if (isExist(d.jobOrderId)) { + return "EPQC"; + } + } + return "IQC"; // Default + }, [itemDetail]); + + const detailMode = useMemo(() => { + const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId); + return isDetailMode; + }, [itemDetail]); // W I P // - const validateFieldFail = (field : FieldPath, condition: boolean, message: string) : boolean => { + const validateFieldFail = (field : FieldPath, condition: boolean, message: string) : boolean => { // console.log("Checking if " + message) if (condition) { setError(field, { message: message}); return false; } else { clearErrors(field); return true; } @@ -166,8 +154,8 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { } else console.log("%c Validated accQty:", "color:yellow", accQty); } - },[setError, qcDecision, accQty, itemDetail]) + },[setError, qcDecision, accQty, itemDetail]) useEffect(() => { // W I P // ----- if (qcDecision == 1) { if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ @@ -213,208 +201,153 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { [], ); - function BooleanEditCell(params: GridRenderEditCellParams) { - const apiRef = useGridApiContext(); - const { id, field, value } = params; - - const handleChange = (e: React.ChangeEvent) => { - apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); - apiRef.current.stopCellEditMode({ id, field }); // commit immediately - }; - - return ; - } - - 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 ( - - {`${index}. ${params.value}`}
- {params.row.description} -
- )}, - }, - { - 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 ( - - { - 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}`} - > - } - label="合格" - disabled={qcDisabled(rowValue)} - sx={{ - color: rowValue.qcPassed === true ? "green" : "inherit", - "& .Mui-checked": {color: "green"} - }} - /> - } - label="不合格" - disabled={qcDisabled(rowValue)} - sx={{ - color: rowValue.qcPassed === false ? "red" : "inherit", - "& .Mui-checked": {color: "red"} - }} - /> - - - ); - }, - }, - { - field: "failQty", - headerName: t("failedQty"), - flex: 0.5, - // editable: true, - renderCell: (params) => { - const index = Number(params.id);//params.api.getRowIndexRelativeToVisibleRows(params.id); - return ( - { - 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 ( - { - const value = e.target.value; - setValue(`qcResult.${index}.remarks`, value); - }} - // onChange={(e) => { - // const remarks = e.target.value; - // // const next = v === '' ? undefined : Number(v); - // // if (Number.isNaN(next)) return; - // // setQcItems((prev) => - // // prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r)) - // // ); - // }} - // {...register(`qcResult.${index}.remarks`, { - // required: "remarks required!", - // })} - onClick={(e) => e.stopPropagation()} - onMouseDown={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - sx={{ width: '100%', - "& .MuiInputBase-input": { - padding: "0.75rem", - fontSize: 24, - }, - }} - /> - ); - }, - }, - ], []) // Set initial value for acceptQty useEffect(() => { if (itemDetail?.demandQty > 0) { //!== undefined) { - setValue("acceptQty", itemDetail.demandQty); // THIS NEED TO UPDATE TO NOT USE DEMAND QTY + setValue("acceptQty", itemDetail.demandQty); // TODO: THIS NEED TO UPDATE TO NOT USE DEMAND QTY } else { setValue("acceptQty", itemDetail?.acceptedQty); } }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); + // Fetch Qc Data useEffect(() => { - console.log("%c Qc Record updated:", "color:green", qcRecord); - if (qcRecord.length < 1) { // New QC - const fetchedQcData = dummyQcData; //TODO fetch from DB - setValue("qcResult", fetchedQcData); - } else { - if (itemDetail?.status == "escalated") { // Copy the previous QC data for editing - if (qcRecord.find((qc) => !isExist(qc.escalationLogId)) === undefined) { - const copiedQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined })); - const mutableQcData = [...qcRecord, ...copiedQcData]; - setValue("qcResult", mutableQcData); - } + // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); + if (itemDetail) { + const d = itemDetail; + fetchNewQcData(d); + if (d.status == "pending") { + // + } else { + fetchQcResultData(d); } + } + }, [itemDetail]); + + const fetchNewQcData = useCallback( + async (input: QcInput) => { + try { + const res = await fetchQcCategory(input.itemId, qcType); + if (res.qcItems.length > 0) { + console.log("%c Fetched Qc Template: ", "color:orange", res); + + setQcCategory(res); + // setQcResult(res.qcItems); + // setValue("qcResult", res.qcItems); + } else throw("Result is undefined"); + } catch (e) { + console.log("%c Error when fetching Qc Template: ", "color:red", e); + alert(t("Missing QC Template, please contact administrator")); + // closeHandler({}, "backdropClick"); + } + },[fetchQcCategory, setValue] + ); + + const fetchQcResultData = useCallback( + async (input: QcInput) => { + try { + const res = await fetchQcResult(input.id); // StockInLineId for now + if (res.length > 0) { + console.log("%c Fetched Qc Result: ", "color:orange", res); + setValue("qcResult", res); + + fetchEscalationLogData(input.id); + // } else {setStockInLineInfo((prev) => ({...prev, qcResult: []} as StockInLine));} + } else throw("Result is undefined"); + } catch (e) { + console.log("%c Error when fetching Qc Result: ", "color:red", e); + // alert("Something went wrong, please retry"); + // closeHandler({}, "backdropClick"); + } + },[fetchQcResult, setValue] + ); + + const fetchEscalationLogData = useCallback( + async (stockInLineId: number) => { + try { + const res = await fetchEscalationLogsByStockInLines([stockInLineId]); + if (res.length > 0) { + console.log("%c Fetched Escalation Log: ", "color:orange", res[0]); + setEscResult(res); + // formProps.setValue("escalationLog", res[0]); + + }// else throw("Result is undefined"); + } catch (e) { + console.log("%c Error when fetching EscalationLog: ", "color:red", e); + // alert("Something went wrong, please retry"); + // closeHandler({}, "backdropClick"); + } + },[fetchEscalationLogsByStockInLines] + ); - if (qcRecord.length > 0) { - if (qcResult.length < 1) { // Set QC Result + // Set QC Data + useEffect(() => { + if (itemDetail) { + const d = itemDetail; + if (qcRecord.length < 1) { // No QC Data + if (d.status == "pending") { // New QC + if (qcCategory) { + if (qcCategory.qcItems.length > 0) { + const filledQcItems = fillQcResult(qcCategory.qcItems); + setValue("qcResult", filledQcItems); + console.log("%c New QC Record applied:", "color:green", filledQcItems); + } + } + } else { + console.log("%c No QC Record loaded:", "color:green"); + // + } + } else { // QC Result fetched + if (qcRecord.some(qc => qc.order !== undefined)) { // If QC Result is filled with order + if (d.status == "escalated") { // Copy the previous QC data for editing + + // If no editable Qc Data + if (!qcRecord.some((qc) => !isExist(qc.escalationLogId))) { + const mutableQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined })); + const copiedQcData = [...mutableQcData, ...qcRecord]; + setValue("qcResult", copiedQcData); + console.log("%c QC Record copied:", "color:green", copiedQcData); + return; + } + } + + // Set QC Result + // const filteredQcResult = qcRecord; const filteredQcResult = qcRecord.filter((qc) => !isExist(qc.escalationLogId)); + console.log("%c QC Result loaded:", "color:green", filteredQcResult); setQcResult(filteredQcResult); - } - if (qcHistory.length < 1) { // Set QC History - const filteredQcHistory = qcRecord.filter((qc) => isExist(qc.escalationLogId)); - setQcHistory(filteredQcHistory); + + // Set QC History + if (filteredQcResult.length < qcRecord.length) { // If there are Qc History + if (qcHistory.length < 1) { + const filteredQcHistory = qcRecord.filter((qc) => isExist(qc.escalationLogId)); + console.log("%c QC History loaded:", "color:green", filteredQcHistory); + setQcHistory(filteredQcHistory); + } + } + + } else { + if (qcCategory) { + const filledQcData = fillQcResult(qcRecord, qcCategory?.qcItems); + console.log("%c QC Result filled:", "color:green", filledQcData); + setValue("qcResult", filledQcData); + } } } } - }, [qcRecord, setValue]) + }, [qcRecord, qcCategory, setValue, itemDetail]) + + const fillQcResult = (qcResults: QcResult[], qcItems: QcData[] = []) => { + let result = [] as QcResult[]; + qcResults.forEach((r, index) => { + const target = qcItems.find((t) => t.qcItemId === r.qcItemId); + const n = { ...target, ...r }; //, id: index }; + result.push(n); + }); + result.sort((a,b) => a.order! - b.order!); + return result; + }; // const [openCollapse, setOpenCollapse] = useState(false) const [isCollapsed, setIsCollapsed] = useState(true); @@ -435,11 +368,6 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { // }, [setValue]); - useEffect(() => { - // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); - - }, [itemDetail]); - const setDefaultQcDecision = (status : string | undefined) => { const param = status?.toLowerCase(); if (param !== undefined && param !== null) { @@ -473,37 +401,46 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { } else { return 60} }; - // For DEMO - const dummyQcData = useMemo(() => { - const d = itemDetail; - if (d.itemId == 23239 || d.itemNo == "PP2277" || d.itemName == "烚意粉") { - return dummyQcData_E2; - } else { - if (d.jobOrderId === null) { - return dummyQcData_A1; - } else { - return dummyQcData_E1; - } - } - }, [itemDetail]) + const formattedDesc = (content: string = "") => { + return ( + <> + {content.split("\\n").map((line, index) => ( + {line}
+ ))} + + ); + } - 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 ( + + N/A + + ); + } else + return ( + <> + + + {qcCategory?.name} ({qcCategory?.code}) + + + 品檢類型:{qcType} + + + {formattedDesc(qcCategory?.description)} + + + + ); + }, [qcType, qcCategory]); return ( <> - {itemDetail ? ( + {(qcRecord.length > 0) ? ( + // {(qcRecord.length > 0 && qcCategory) ? ( = ({ itemDetail, disabled = false }) => { variant="scrollable" > - {(itemDetail.escResult && itemDetail.escResult?.length > 0) && + {(escResult && escResult?.length > 0) && ()} {tabIndex == 0 && ( <> - - - - {dummyQcHeader.name} - - - 品檢類型:{dummyQcHeader.type} - - - {dummyQcHeader.description} - - + {/* apiRef={apiRef} columns={qcColumns} _formKey="qcResult" validateRow={validation} /> */} - 0 ? qcResult : qcItems} - // rows={disabled? qcResult:qcItems} - // autoHeight - sortModel={[]} - // getRowHeight={getRowHeight} - getRowHeight={() => 'auto'} - getRowId={getRowId} + disabled={disabled} /> @@ -571,26 +490,12 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { */} - + - - - {dummyQcHeader.name} - - - 品檢類型:{dummyQcHeader.type} - - - {dummyQcHeader.description} - - - + 0 ? qcResult : qcItems} - // rows={disabled? qcResult:qcItems} - autoHeight - sortModel={[]} /> @@ -712,7 +617,7 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { } sx={{"& .Mui-checked": {color: "blue"}}} - label="上報品檢結果" /> + label="暫時存放到置物區,並等待品檢結果" /> )} @@ -730,18 +635,6 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false }) => { setIsCollapsed={setIsCollapsed} /> )} - {/* {qcAccept && - - {t("Escalation Result")} - - - - - } */} ) : } diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx new file mode 100644 index 0000000..10d46d4 --- /dev/null +++ b/src/components/Qc/QcForm.tsx @@ -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 = ({ 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(); + + 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) => { + apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); + apiRef.current.stopCellEditMode({ id, field }); // commit immediately + }; + + return ; + } + + 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 ( + + {`${params.row.order ?? "N/A"}. ${params.value}`}
+ {params.row.description} +
+ )}, + }, + { + 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 ( + + { + 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}`} + > + } + label="合格" + disabled={qcDisabled(rowValue)} + sx={{ + color: rowValue.qcPassed === true ? "green" : "inherit", + "& .Mui-checked": {color: "green"} + }} + /> + } + label="不合格" + disabled={qcDisabled(rowValue)} + sx={{ + color: rowValue.qcPassed === false ? "red" : "inherit", + "& .Mui-checked": {color: "red"} + }} + /> + + + ); + }, + }, + { + 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 ( + { + 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 ( + { + 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 ( + <> + 'auto'} + /> + + ); +}; +export default QcForm; diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx new file mode 100644 index 0000000..2edd7a6 --- /dev/null +++ b/src/components/Qc/QcStockInModal.tsx @@ -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 { + // 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 = ({ + open, + onClose, + // itemDetail, + inputDetail, + session, + warehouse, + printerCombo, + skipQc = false, +}) => { + const { + t, + i18n: { language }, + } = useTranslation("purchaseOrder"); + + const [stockInLineInfo, setStockInLineInfo] = useState(); + const [isLoading, setIsLoading] = useState(false); + // const [skipQc, setSkipQc] = useState(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>( + (_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({ + defaultValues: { + ...defaultNewValue, + }, + }); + + const closeHandler = useCallback>( + () => { + 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>( + 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>( + async (data, event) => { + console.log("Error", data); + }, [] + ); + + // QC submission handler + const onSubmitQc = useCallback>( + 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({}) + const [pafRowSelectionModel, setPafRowSelectionModel] = useState([]) + 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>( + 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 ( + <> + + + + {(!isLoading && stockInLineInfo) ? (<> + + + + {showPutaway && } + + + + + {tabIndex === 0 && + + + + {t("Delivery Detail")} + + + {stockInLineInfo.jobOrderId ? ( + + ) : ( + + ) + } + {skipQc === false && ( + ) + } + + {(!viewOnly && !showPutaway) && ()} + + + } + + {tabIndex === 1 && + + + + } + + + + {tabIndex == 1 && ( + + { + setSelectedPrinter(value) + }} + renderInput={(params) => ( + + )} + /> + { + event.target.value = event.target.value.replace(/[^0-9]/g, '') + + setPrintQty(Number(event.target.value)) + }} + sx={{ width: 300}} + /> + + + + )} + ) : } + + + + + ); +}; +export default QcStockInModal; diff --git a/src/components/PoDetail/dummyQcTemplate.tsx b/src/components/Qc/dummyQcTemplate.tsx similarity index 100% rename from src/components/PoDetail/dummyQcTemplate.tsx rename to src/components/Qc/dummyQcTemplate.tsx