"use client" import { SearchJoResultRequest, fetchJos, updateJo } 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 } from "@mui/material"; import { BomCombo } from "@/app/api/bom"; import JoCreateFormModal from "./JoCreateFormModal"; import AddIcon from '@mui/icons-material/Add'; 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"; 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()); 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(); for (const jo of filteredJos) { try { const detailedJo = await fetchJoDetailClient(jo.id); detailedMap.set(jo.id, detailedJo); } catch (error) { console.error(`Error fetching detail for JO ${jo.id}:`, error); } } setDetailedJos(detailedMap); }; if (filteredJos.length > 0) { fetchDetailedJos(); } }, [filteredJos]); useEffect(() => { const fetchInventoryData = async () => { try { const inventoryResponse = await fetchInventories({ code: "", name: "", type: "", pageNum: 0, pageSize: 1000 }); 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 columns = useMemo[]>( () => [ { name: "code", label: t("Code"), flex: 2 }, { name: "item", label: `${t("Item Name")}`, renderCell: (row) => { return row.item ? <>{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: "status", label: t("Status"), renderCell: (row) => { return {t(upperFirst(row.status))} } },{ name: "planStart", label: t("Estimated Production Date"), align: "left", headerAlign: "left", renderCell: (row) => { return row.planStart ? arrayToDateString(row.planStart) : '-' } }, { 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: "jobTypeName", label: t("Job Type"), renderCell: (row) => { return row.jobTypeName ? t(row.jobTypeName) : '-' } }, { name: "id", label: t("Actions"), renderCell: (row) => { return ( ) } }, ], [t, inventoryData, detailedJos] ) // 按照 PoSearch 的模式:创建 newPageFetch 函数 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); // 后端已经按 id DESC 排序,不需要再次排序 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([]); } }, [], ); // 按照 PoSearch 的模式:使用相同的 useEffect 逻辑 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")); // 重置为默认输入,让 useEffect 自动触发 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} /> { }} /> } export default JoSearch;