|
- "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<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());
-
- // 合并后的统一编辑 Dialog 状态
- const [openEditDialog, setOpenEditDialog] = useState(false);
- const [selectedJoForEdit, setSelectedJoForEdit] = useState<JobOrder | null>(null);
- const [editPlanStartDate, setEditPlanStartDate] = useState<dayjs.Dayjs | null>(null);
- const [editReqQtyMultiplier, setEditReqQtyMultiplier] = useState<number>(1);
- const [editBomForReqQty, setEditBomForReqQty] = useState<BomCombo | null>(null);
- const [editProductionPriority, setEditProductionPriority] = useState<number>(50);
- const [editProductProcessId, setEditProductProcessId] = useState<number | null>(null);
-
- 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>();
- 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<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 fetchBomForJo = useCallback(async (jo: JobOrder): Promise<BomCombo | null> => {
- 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<Column<JobOrder>[]>(
- () => [
-
- {
- name: "planStart",
- label: t("Estimated Production Date"),
- align: "left",
- headerAlign: "left",
- renderCell: (row) => {
- return (
- <Stack direction="row" alignItems="center" spacing={1}>
- {row.status == "planning" && (
- <IconButton
- size="small"
- onClick={(e) => {
- e.stopPropagation();
- handleOpenEditDialog(row);
- }}
- sx={{ padding: '4px' }}
- >
- <EditIcon fontSize="small" />
- </IconButton>
- )}
- <span>{row.planStart ? arrayToDateString(row.planStart) : '-'}</span>
-
- </Stack>
- );
- }
- },
- {
- name: "productionPriority",
- label: t("Production Priority"),
- renderCell: (row) => {
- return (
- <Stack direction="row" alignItems="center" spacing={1}>
- <span>{integerFormatter.format(row.productionPriority)}</span>
-
- </Stack>
- );
- }
- },
- {
- 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 (
- <Stack direction="row" alignItems="center" spacing={1} justifyContent="flex-end">
- <span>{integerFormatter.format(row.reqQty)}</span>
-
- </Stack>
- );
- }
- },
- {
- 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 (
- <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
- {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
- </span>
- );
- }
- },
- {
- name: "status",
- label: t("Status"),
- renderCell: (row) => {
- return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
- {t(upperFirst(row.status))}
- </span>
- }
- },
- {
- 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, 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<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}
- jobTypes={jobTypes}
- onClose={onCloseCreateJoModal}
- onSearch={() => {
- setInputs({ ...defaultInputs });
- setPagingController(defaultPagingController);
- }}
- />
-
- <QcStockInModal
- session={sessionToken}
- open={openModal}
- onClose={closeNewModal}
- inputDetail={modalInfo}
- printerCombo={printerCombo}
- />
-
- {/* 合并后的统一编辑 Dialog */}
- <Dialog
- open={openEditDialog}
- onClose={handleCloseEditDialog}
- fullWidth
- maxWidth="sm"
- >
- <DialogTitle>{t("Edit Job Order")}</DialogTitle>
- <DialogContent>
- <Stack spacing={3} sx={{ mt: 1 }}>
- {/* Plan Start Date */}
- <LocalizationProvider dateAdapter={AdapterDayjs}>
- <DatePicker
- label={t("Estimated Production Date")}
- value={editPlanStartDate}
- onChange={(newValue) => setEditPlanStartDate(newValue)}
- slotProps={{
- textField: {
- fullWidth: true,
- margin: "dense",
- }
- }}
- />
- </LocalizationProvider>
-
- {/* Production Priority */}
- <TextField
- label={t("Production Priority")}
- type="number"
- fullWidth
- value={editProductionPriority}
- onChange={(e) => {
- const val = Number(e.target.value);
- if (val >= 1 && val <= 100) {
- setEditProductionPriority(val);
- }
- }}
- inputProps={{
- min: 1,
- max: 100,
- step: 1
- }}
- />
-
- {/* ReqQty */}
- {editBomForReqQty && (
- <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
- <TextField
- label={t("Base Qty")}
- fullWidth
- type="number"
- variant="outlined"
- value={editBomForReqQty.outputQty}
- disabled
- InputProps={{
- endAdornment: editBomForReqQty.outputQtyUom ? (
- <InputAdornment position="end">
- <Typography variant="body2" sx={{ color: "text.secondary" }}>
- {editBomForReqQty.outputQtyUom}
- </Typography>
- </InputAdornment>
- ) : null
- }}
- sx={{ flex: 1 }}
- />
- <Typography variant="body1" sx={{ color: "text.secondary" }}>
- ×
- </Typography>
- <TextField
- label={t("Batch Count")}
- fullWidth
- type="number"
- variant="outlined"
- value={editReqQtyMultiplier}
- onChange={(e) => {
- 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 }}
- />
- <Typography variant="body1" sx={{ color: "text.secondary" }}>
- =
- </Typography>
- <TextField
- label={t("Req. Qty")}
- fullWidth
- variant="outlined"
- type="number"
- value={editBomForReqQty ? (editReqQtyMultiplier * editBomForReqQty.outputQty) : ""}
- disabled
- InputProps={{
- endAdornment: editBomForReqQty.outputQtyUom ? (
- <InputAdornment position="end">
- <Typography variant="body2" sx={{ color: "text.secondary" }}>
- {editBomForReqQty.outputQtyUom}
- </Typography>
- </InputAdornment>
- ) : null
- }}
- sx={{ flex: 1 }}
- />
- </Box>
- )}
- </Stack>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleCloseEditDialog}>{t("Cancel")}</Button>
- <Button
- variant="contained"
- onClick={handleConfirmEdit}
- disabled={!editPlanStartDate || !editBomForReqQty}
- >
- {t("Save")}
- </Button>
- </DialogActions>
- </Dialog>
- </>
- }
-
- export default JoSearch;
|