|
- "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<Omit<JobOrder, "id">>;
- type SearchParamNames = keyof SearchQuery;
-
- const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
- const { t } = useTranslation("jo");
- const router = useRouter()
- const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
- const [inputs, setInputs] = useState(defaultInputs);
- const [pagingController, setPagingController] = useState(
- defaultPagingController
- )
- const [totalCount, setTotalCount] = useState(0)
- const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)
-
- const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
- const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
-
- const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
- 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<number, JobOrder>();
-
- 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<SearchParamNames>[] = 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<Column<JobOrder>[]>(
- () => [
- {
- 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 <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
- {t(upperFirst(row.status))}
- </span>
- }
- },{
- 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 (
- <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
- {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
- </span>
- );
- }
- },
- {
- name: "jobTypeName",
- label: t("Job Type"),
- renderCell: (row) => {
- return row.jobTypeName ? t(row.jobTypeName) : '-'
- }
- },
- {
- name: "id",
- label: t("Actions"),
- renderCell: (row) => {
- return (
- <Button
- id="emailSupplier"
- type="button"
- variant="contained"
- color="primary"
- sx={{ width: "150px" }}
- onClick={() => onDetailClick(row)}
- >
- {t("View")}
- </Button>
- )
- }
- },
- ], [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<boolean>(false);
- const [modalInfo, setModalInfo] = useState<StockInLineInput>();
-
- 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<SearchParamNames, string>) => {
- 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 <>
- <Stack
- direction="row"
- justifyContent="flex-end"
- spacing={2}
- sx={{ mt: 2 }}
- >
- <Button
- variant="outlined"
- startIcon={<AddIcon />}
- onClick={onOpenCreateJoModal}
- >
- {t("Create Job Order")}
- </Button>
- </Stack>
- <SearchBox
- criteria={searchCriteria}
- onSearch={onSearch}
- onReset={onReset}
- />
- <SearchResults<JobOrder>
- items={filteredJos}
- columns={columns}
- setPagingController={setPagingController}
- pagingController={pagingController}
- totalCount={totalCount}
- isAutoPaging={false}
- />
- <JoCreateFormModal
- open={isCreateJoModalOpen}
- bomCombo={bomCombo}
- onClose={onCloseCreateJoModal}
- onSearch={() => {
-
- }}
- />
-
- <QcStockInModal
- session={sessionToken}
- open={openModal}
- onClose={closeNewModal}
- inputDetail={modalInfo}
- printerCombo={printerCombo}
- />
- </>
- }
-
- export default JoSearch;
|