"use client" import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Criterion } from "../SearchBox"; import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults"; import { EditNote } from "@mui/icons-material"; import { arrayToDateString, arrayToDateTimeString, integerFormatter, dayjsToDateString } from "@/app/utils/formatUtil"; import { orderBy, uniqBy, upperFirst } from "lodash"; import SearchBox from "../SearchBox/SearchBox"; import { useRouter } from "next/navigation"; import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; import { StockInLineInput } from "@/app/api/stockIn"; import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box } from "@mui/material"; import { BomCombo } from "@/app/api/bom"; import JoCreateFormModal from "./JoCreateFormModal"; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; import QcStockInModal from "../Qc/QcStockInModal"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { createStockInLine } from "@/app/api/stockIn/actions"; import { msg } from "../Swal/CustomAlerts"; import dayjs from "dayjs"; //import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { PrinterCombo } from "@/app/api/settings/printer"; import { JobTypeResponse } from "@/app/api/jo/actions"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { updateJoPlanStart } from "@/app/api/jo/actions"; import { arrayToDayjs } from "@/app/utils/formatUtil"; interface Props { defaultInputs: SearchJoResultRequest, bomCombo: BomCombo[] printerCombo: PrinterCombo[]; jobTypes: JobTypeResponse[]; } type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => { const { t } = useTranslation("jo"); const router = useRouter() const [filteredJos, setFilteredJos] = useState([]); const [inputs, setInputs] = useState(defaultInputs); const [pagingController, setPagingController] = useState( defaultPagingController ) const [totalCount, setTotalCount] = useState(0) const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); // 合并后的统一编辑 Dialog 状态 const [openEditDialog, setOpenEditDialog] = useState(false); const [selectedJoForEdit, setSelectedJoForEdit] = useState(null); const [editPlanStartDate, setEditPlanStartDate] = useState(null); const [editReqQtyMultiplier, setEditReqQtyMultiplier] = useState(1); const [editBomForReqQty, setEditBomForReqQty] = useState(null); const [editProductionPriority, setEditProductionPriority] = useState(50); const [editProductProcessId, setEditProductProcessId] = useState(null); const fetchJoDetailClient = async (id: number): Promise => { const response = await fetch(`/api/jo/detail?id=${id}`); if (!response.ok) { throw new Error('Failed to fetch JO detail'); } return response.json(); }; useEffect(() => { const fetchDetailedJos = async () => { const detailedMap = new Map(); try { const results = await Promise.all( filteredJos.map((jo) => fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => { console.error(`Error fetching detail for JO ${jo.id}:`, error); return null; }) ) ); results.forEach((r) => { if (r) detailedMap.set(r.id, r.detail); }); } catch (error) { console.error("Error fetching JO details:", error); } setDetailedJos(detailedMap); }; if (filteredJos.length > 0) { fetchDetailedJos(); } }, [filteredJos]); /* useEffect(() => { const fetchInventoryData = async () => { try { const inventoryResponse = await fetchInventories({ code: "", name: "", type: "", pageNum: 0, pageSize: 200, }); setInventoryData(inventoryResponse.records ?? []); } catch (error) { console.error("Error fetching inventory data:", error); } }; fetchInventoryData(); }, []); */ const getStockAvailable = (pickLine: JoDetailPickLine) => { const inventory = inventoryData.find(inventory => inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name ); if (inventory) { return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); } return 0; }; const isStockSufficient = (pickLine: JoDetailPickLine) => { const stockAvailable = getStockAvailable(pickLine); return stockAvailable >= pickLine.reqQty; }; const getStockCounts = (jo: JobOrder) => { return { sufficient: jo.sufficientCount, insufficient: jo.insufficientCount }; }; const searchCriteria: Criterion[] = useMemo(() => [ { label: t("Code"), paramName: "code", type: "text" }, { label: t("Item Name"), paramName: "itemName", type: "text" }, { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: { from: dayjsToDateString(dayjs(), "input"), to: dayjsToDateString(dayjs(), "input") } }, { label: t("Job Type"), paramName: "jobTypeName", type: "select", options: jobTypes.map(jt => jt.name) }, ], [t, jobTypes]) const fetchBomForJo = useCallback(async (jo: JobOrder): Promise => { try { const detailedJo = detailedJos.get(jo.id) || await fetchJoDetailClient(jo.id); const matchingBom = bomCombo.find(bom => { return true; // 临时占位 }); return matchingBom || null; } catch (error) { console.error("Error fetching BOM for JO:", error); return null; } }, [bomCombo, detailedJos]); // 统一的打开编辑对话框函数 const handleOpenEditDialog = useCallback(async (jo: JobOrder) => { setSelectedJoForEdit(jo); // 设置 Plan Start Date if (jo.planStart && Array.isArray(jo.planStart)) { setEditPlanStartDate(arrayToDayjs(jo.planStart)); } else { setEditPlanStartDate(dayjs()); } // 设置 Production Priority setEditProductionPriority(jo.productionPriority ?? 50); // 获取 productProcessId try { const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions"); const processes = await fetchProductProcessesByJobOrderId(jo.id); if (processes && processes.length > 0) { setEditProductProcessId(processes[0].id); } } catch (error) { console.error("Error fetching product process:", error); } // 设置 ReqQty const bom = await fetchBomForJo(jo); if (bom) { setEditBomForReqQty(bom); const currentMultiplier = bom.outputQty > 0 ? Math.round(jo.reqQty / bom.outputQty) : 1; setEditReqQtyMultiplier(currentMultiplier); } setOpenEditDialog(true); }, [fetchBomForJo]); // 统一的关闭函数 const handleCloseEditDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { setOpenEditDialog(false); setSelectedJoForEdit(null); setEditPlanStartDate(null); setEditReqQtyMultiplier(1); setEditBomForReqQty(null); setEditProductionPriority(50); setEditProductProcessId(null); }, []); const columns = useMemo[]>( () => [ { name: "planStart", label: t("Estimated Production Date"), align: "left", headerAlign: "left", renderCell: (row) => { return ( {row.status == "planning" && ( { e.stopPropagation(); handleOpenEditDialog(row); }} sx={{ padding: '4px' }} > )} {row.planStart ? arrayToDateString(row.planStart) : '-'} ); } }, { name: "productionPriority", label: t("Production Priority"), renderCell: (row) => { return ( {integerFormatter.format(row.productionPriority)} ); } }, { name: "code", label: t("Code"), flex: 2 }, { name: "item", label: `${t("Item Name")}`, renderCell: (row) => { return row.item ? <>{t(row.jobTypeName)} {t(row.item.code)} {t(row.item.name)} : '-' } }, { name: "reqQty", label: t("Req. Qty"), align: "right", headerAlign: "right", renderCell: (row) => { return ( {integerFormatter.format(row.reqQty)} ); } }, { name: "item", label: t("UoM"), align: "left", headerAlign: "left", renderCell: (row) => { return row.item?.uom ? t(row.item.uom.udfudesc) : '-' } }, { name: "stockStatus" as keyof JobOrder, label: t("BOM Status"), align: "left", headerAlign: "left", renderCell: (row) => { const stockCounts = getStockCounts(row); return ( 0 ? 'red' : 'green' }}> {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient} ); } }, { name: "status", label: t("Status"), renderCell: (row) => { return {t(upperFirst(row.status))} } }, { name: "id", label: t("Actions"), renderCell: (row) => { return ( ) } }, ], [t, inventoryData, detailedJos, handleOpenEditDialog] ) const newPageFetch = useCallback( async ( pagingController: { pageNum: number; pageSize: number }, filterArgs: SearchJoResultRequest, ) => { const params: SearchJoResultRequest = { ...filterArgs, pageNum: pagingController.pageNum - 1, pageSize: pagingController.pageSize, }; const response = await fetchJos(params); console.log("newPageFetch params:", params) console.log("newPageFetch response:", response) if (response && response.records) { console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); setTotalCount(response.total); setFilteredJos(response.records); console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); } else { console.warn("newPageFetch - no response or no records"); setFilteredJos([]); } }, [], ); const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { try { const response = await updateJoReqQty({ id: jobOrderId, reqQty: newReqQty }); if (response) { msg(t("update success")); await newPageFetch(pagingController, inputs); } } catch (error) { console.error("Error updating reqQty:", error); msg(t("update failed")); } }, [pagingController, inputs, newPageFetch, t]); const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { const response = await updateJoPlanStart({ id: jobOrderId, planStart }); if (response) { await newPageFetch(pagingController, inputs); } }, [pagingController, inputs, newPageFetch]); const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { const response = await updateProductProcessPriority(productProcessId, productionPriority) if (response) { await newPageFetch(pagingController, inputs); } }, [pagingController, inputs, newPageFetch]); // 统一的确认函数 const handleConfirmEdit = useCallback(async () => { if (!selectedJoForEdit) return; try { // 更新 Plan Start if (editPlanStartDate) { const dateString = `${dayjsToDateString(editPlanStartDate, "input")}T00:00:00`; await handleUpdatePlanStart(selectedJoForEdit.id, dateString); } // 更新 ReqQty if (editBomForReqQty) { const newReqQty = editReqQtyMultiplier * editBomForReqQty.outputQty; await handleUpdateReqQty(selectedJoForEdit.id, newReqQty); } // 更新 Production Priority if (editProductProcessId) { await handleUpdateOperationPriority(editProductProcessId, Number(editProductionPriority)); } setOpenEditDialog(false); setSelectedJoForEdit(null); setEditPlanStartDate(null); setEditReqQtyMultiplier(1); setEditBomForReqQty(null); setEditProductionPriority(50); setEditProductProcessId(null); } catch (error) { console.error("Error updating:", error); msg(t("update failed")); } }, [selectedJoForEdit, editPlanStartDate, editBomForReqQty, editReqQtyMultiplier, editProductionPriority, editProductProcessId, handleUpdatePlanStart, handleUpdateReqQty, handleUpdateOperationPriority, t]); useEffect(() => { newPageFetch(pagingController, inputs); }, [newPageFetch, pagingController, inputs]); const handleUpdate = useCallback(async (jo: JobOrder) => { console.log(jo); try { if (jo.id) { const response = await updateJo({ id: jo.id, status: "storing" }); console.log(`%c Updated JO:`, "color:lime", response); const postData = { itemId: jo?.item?.id!!, acceptedQty: jo?.reqQty ?? 1, productLotNo: jo?.code, productionDate: arrayToDateString(dayjs(), "input"), jobOrderId: jo?.id, }; const res = await createStockInLine(postData); console.log(`%c Created Stock In Line`, "color:lime", res); msg(t("update success")); setInputs(defaultInputs); setPagingController(defaultPagingController); } } catch (e) { console.log(e); } finally { // setIsUploading(false) } }, [defaultInputs, t]) const getButtonSx = (jo : JobOrder) => { const joStatus = jo.status?.toLowerCase(); const silStatus = jo.stockInLineStatus?.toLowerCase(); let btnSx = {label:"", color:""}; switch (joStatus) { case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break; case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break; case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break; case "storing": switch (silStatus) { case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break; case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break; case "escalated": if (sessionToken?.id == jo.silHandlerId) { btnSx = {label: t("escalation processing"), color:"warning.main"}; break; } default: btnSx = {label: t("view stockin"), color:"info.main"}; } break; case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break; default: btnSx = {label: t("scan picked material"), color:"success.main"}; } return btnSx }; const { data: session } = useSession(); const sessionToken = session as SessionWithTokens | null; const [openModal, setOpenModal] = useState(false); const [modalInfo, setModalInfo] = useState(); const onDetailClick = useCallback((record: JobOrder) => { router.push(`/jo/edit?id=${record.id}`) }, [router]) const closeNewModal = useCallback(() => { setOpenModal(false); setInputs(defaultInputs); setPagingController(defaultPagingController); }, [defaultInputs]); const onSearch = useCallback((query: Record) => { const transformedQuery = { ...query, planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart, planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo, jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" }; setInputs({ code: transformedQuery.code, itemName: transformedQuery.itemName, planStart: transformedQuery.planStart, planStartTo: transformedQuery.planStartTo, jobTypeName: transformedQuery.jobTypeName }); setPagingController(defaultPagingController); }, [defaultInputs]) const onReset = useCallback(() => { setInputs(defaultInputs); setPagingController(defaultPagingController); }, [defaultInputs]) const onOpenCreateJoModal = useCallback(() => { setIsCreateJoModalOpen(() => true) }, []) const onCloseCreateJoModal = useCallback(() => { setIsCreateJoModalOpen(() => false) }, []) return <> items={filteredJos} columns={columns} setPagingController={setPagingController} pagingController={pagingController} totalCount={totalCount} isAutoPaging={false} /> { setInputs({ ...defaultInputs }); setPagingController(defaultPagingController); }} /> {/* 合并后的统一编辑 Dialog */} {t("Edit Job Order")} {/* Plan Start Date */} setEditPlanStartDate(newValue)} slotProps={{ textField: { fullWidth: true, margin: "dense", } }} /> {/* Production Priority */} { const val = Number(e.target.value); if (val >= 1 && val <= 100) { setEditProductionPriority(val); } }} inputProps={{ min: 1, max: 100, step: 1 }} /> {/* ReqQty */} {editBomForReqQty && ( {editBomForReqQty.outputQtyUom} ) : null }} sx={{ flex: 1 }} /> × { const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value))); setEditReqQtyMultiplier(val); }} inputProps={{ min: 1, step: 1 }} sx={{ flex: 1 }} /> = {editBomForReqQty.outputQtyUom} ) : null }} sx={{ flex: 1 }} /> )} } export default JoSearch;