From d65e3db136ed8ed8b71224924de8b64821cd4a0f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 18 Mar 2026 01:15:09 +0800 Subject: [PATCH] update --- src/app/api/jo/actions.ts | 11 +- src/components/JoSearch/JoSearch.tsx | 197 ++++++++++++++---- src/components/Jodetail/JoPickOrderList.tsx | 78 +++++-- .../Jodetail/newJobPickExecution.tsx | 28 +++ src/components/Qc/QcComponent.tsx | 16 +- src/components/Qc/QcStockInModal.tsx | 10 +- src/i18n/zh/common.json | 2 + 7 files changed, 267 insertions(+), 75 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index a4e7931..7543959 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -532,6 +532,8 @@ export interface AllJoPickOrderResponse { jobOrderStatus: string; finishedPickOLineCount: number; floorPickCounts: FloorPickCount[]; + noLotPickCount?: FloorPickCount | null; + suggestedFailCount?: number; } export interface UpdateJoPickOrderHandledByRequest { pickOrderId: number; @@ -689,10 +691,11 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder }, ); }); -export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null) => { - const query = isDrink !== undefined && isDrink !== null - ? `?isDrink=${isDrink}` - : ""; +export const fetchAllJoPickOrders = cache(async (isDrink?: boolean | null, floor?: string | null) => { + const params = new URLSearchParams(); + if (isDrink !== undefined && isDrink !== null) params.set("isDrink", String(isDrink)); + if (floor) params.set("floor", floor); + const query = params.toString() ? `?${params.toString()}` : ""; return serverFetchJson( `${BASE_API_URL}/jo/AllJoPickOrder${query}`, { method: "GET" } diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 1206935..ed34f30 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -1,5 +1,5 @@ "use client" -import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; +import { SearchJoResultRequest, fetchJos, releaseJo, 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"; @@ -12,7 +12,7 @@ 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 { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box, CircularProgress } from "@mui/material"; import { BomCombo } from "@/app/api/bom"; import JoCreateFormModal from "./JoCreateFormModal"; import AddIcon from '@mui/icons-material/Add'; @@ -55,6 +55,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); + const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]); + const [releasingJoIds, setReleasingJoIds] = useState>(new Set()); + const [isBatchReleasing, setIsBatchReleasing] = useState(false); // 合并后的统一编辑 Dialog 状态 const [openEditDialog, setOpenEditDialog] = useState(false); @@ -229,9 +232,108 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT setEditProductionPriority(50); setEditProductProcessId(null); }, []); + + 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 isPlanningJo = useCallback((jo: JobOrder) => { + return String(jo.status ?? "").toLowerCase() === "planning"; + }, []); + + const handleReleaseJo = useCallback(async (joId: number) => { + if (!joId) return; + setReleasingJoIds((prev) => { + const next = new Set(prev); + next.add(joId); + return next; + }); + try { + const response = await releaseJo({ id: joId }); + if (response) { + msg(t("update success")); + setCheckboxIds((prev) => prev.filter((id) => Number(id) !== joId)); + await newPageFetch(pagingController, inputs); + } + } catch (error) { + console.error("Error releasing JO:", error); + msg(t("update failed")); + } finally { + setReleasingJoIds((prev) => { + const next = new Set(prev); + next.delete(joId); + return next; + }); + } + }, [inputs, pagingController, t, newPageFetch]); + + const selectedPlanningJoIds = useMemo(() => { + const selectedIds = new Set(checkboxIds.map((id) => Number(id))); + return filteredJos + .filter((jo) => selectedIds.has(jo.id)) + .filter((jo) => isPlanningJo(jo)) + .map((jo) => jo.id); + }, [checkboxIds, filteredJos, isPlanningJo]); + + const handleBatchRelease = useCallback(async () => { + if (selectedPlanningJoIds.length === 0) return; + setIsBatchReleasing(true); + try { + const results = await Promise.allSettled( + selectedPlanningJoIds.map((id) => releaseJo({ id })), + ); + const successCount = results.filter((r) => r.status === "fulfilled").length; + const failedCount = results.length - successCount; + if (successCount > 0 && failedCount === 0) { + msg(t("update success")); + } else if (successCount > 0) { + msg(`${t("update success")} (${successCount}), ${t("update failed")} (${failedCount})`); + } else { + msg(t("update failed")); + } + setCheckboxIds((prev) => prev.filter((id) => !selectedPlanningJoIds.includes(Number(id)))); + await newPageFetch(pagingController, inputs); + } catch (error) { + console.error("Error batch releasing JOs:", error); + msg(t("update failed")); + } finally { + setIsBatchReleasing(false); + } + }, [inputs, pagingController, selectedPlanningJoIds, t, newPageFetch]); const columns = useMemo[]>( () => [ + { + name: "id", + label: "", + type: "checkbox", + disabled: (row) => { + const id = row.id; + return !isPlanningJo(row) || releasingJoIds.has(id) || isBatchReleasing; + } + }, { name: "planStart", @@ -340,49 +442,43 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT name: "id", label: t("Actions"), renderCell: (row) => { + const isPlanning = isPlanningJo(row); + const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing; return ( - + + + + ) } }, - ], [t, inventoryData, detailedJos, handleOpenEditDialog] + ], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing] ) - 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({ @@ -560,6 +656,27 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT spacing={2} sx={{ mt: 2 }} > + + + {t("Selected")}: {selectedPlanningJoIds.length} + + + + + + + + + + + {t("Total pick orders")}: {pickOrders.length} - {paged.map((pickOrder) => { + {pickOrders.map((pickOrder) => { const status = String(pickOrder.jobOrderStatus || ""); const statusLower = status.toLowerCase(); const statusColor = @@ -175,6 +201,22 @@ const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ {floor}: {finishedCount}/{totalCount} ))} + {!!pickOrder.noLotPickCount && ( + + {t("No Lot")}: {pickOrder.noLotPickCount.finishedCount}/{pickOrder.noLotPickCount.totalCount} + + )} + {typeof pickOrder.suggestedFailCount === "number" && pickOrder.suggestedFailCount > 0 && ( + + {t("Suggested Fail")}: {pickOrder.suggestedFailCount} + + )} {statusLower !== "pending" && finishedCount > 0 && ( @@ -202,16 +244,6 @@ const JoPickOrderList: React.FC = ({ onSwitchToRecordTab }) =>{ ); })} - {pickOrders.length > 0 && ( - setPage(p)} - rowsPerPageOptions={[PER_PAGE]} - /> - )} )} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 42644c0..b36cf7a 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -2003,6 +2003,32 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { // ✅ 使用 batchSubmitList API const result = await batchSubmitList(request); console.log(`📥 Batch submit result:`, result); + + // ✅ After batch submit, explicitly trigger completion check per consoCode. + // Otherwise pick_order/job_order may stay RELEASED even when all lines are completed. + try { + const consoCodes = Array.from( + new Set( + lines + .map((l) => (l.pickOrderConsoCode || "").trim()) + .filter((c) => c.length > 0), + ), + ); + if (consoCodes.length > 0) { + await Promise.all( + consoCodes.map(async (code) => { + try { + const completionResponse = await checkAndCompletePickOrderByConsoCode(code); + console.log(`✅ Pick order completion check (${code}):`, completionResponse); + } catch (e) { + console.error(`❌ Error checking completion for ${code}:`, e); + } + }), + ); + } + } catch (e) { + console.error("❌ Error triggering completion checks after batch submit:", e); + } // 刷新数据 const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined; @@ -2118,6 +2144,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const issueData = { ...data, type: "Jo", // Delivery Order Record 类型 + pickerName: session?.user?.name || undefined, + handledBy: currentUserId || undefined, }; const result = await recordPickExecutionIssue(issueData); diff --git a/src/components/Qc/QcComponent.tsx b/src/components/Qc/QcComponent.tsx index f0ab5b1..9dea1b0 100644 --- a/src/components/Qc/QcComponent.tsx +++ b/src/components/Qc/QcComponent.tsx @@ -126,6 +126,10 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLay } return "IQC"; // Default }, [itemDetail]); + + const isJobOrder = useMemo(() => { + return isExist(itemDetail?.jobOrderId); + }, [itemDetail?.jobOrderId]); const detailMode = useMemo(() => { const isDetailMode = itemDetail.status == "escalated" || isExist(itemDetail.jobOrderId); @@ -146,7 +150,7 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLay if (isNaN(accQty) || accQty === undefined || accQty === null || typeof(accQty) != "number") { setError("acceptQty", { message: t("value must be a number") }); } else - if (accQty > itemDetail.acceptedQty) { + if (!isJobOrder && accQty > itemDetail.acceptedQty) { setError("acceptQty", { message: `${t("acceptQty must not greater than")} ${ itemDetail.acceptedQty}` }); } else @@ -156,10 +160,10 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLay console.log("%c Validated accQty:", "color:yellow", accQty); } - },[setError, qcDecision, accQty, itemDetail]) + },[setError, qcDecision, accQty, itemDetail, isJobOrder]) useEffect(() => { // W I P // ----- if (qcDecision == 1) { - if (validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ + if (!isJobOrder && validateFieldFail("acceptQty", accQty > itemDetail.acceptedQty, `${t("acceptQty must not greater than")} ${ itemDetail.acceptedQty}`)) return; if (validateFieldFail("acceptQty", accQty <= 0, t("minimal value is 1"))) return; @@ -188,7 +192,7 @@ const QcComponent: React.FC = ({ itemDetail, disabled = false, compactLay } // console.log("Validated without errors"); -}, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult]); +}, [accQty, qcDecision, watch("qcResult"), qcCategory, qcResult, isJobOrder]); useEffect(() => { clearErrors(); @@ -612,7 +616,7 @@ useEffect(() => { } e.target.value = r; }} - inputProps={{ min: 0.01, max:itemDetail.acceptedQty, step: 0.01 }} + inputProps={isJobOrder ? { min: 0.01, step: 0.01 } : { min: 0.01, max: itemDetail.acceptedQty, step: 0.01 }} // onChange={(e) => { // const inputValue = e.target.value; // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { @@ -632,7 +636,7 @@ useEffect(() => { sx={{ width: '150px' }} value={ (!Boolean(errors.acceptQty) && qcDecision !== undefined && qcDecision != 3) ? - (qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty) + (qcDecision == 1 ? Math.max(0, itemDetail.acceptedQty - accQty) : itemDetail.acceptedQty) : "" } error={Boolean(errors.acceptQty)} diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 0713934..b0c8c85 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -404,6 +404,7 @@ const QcStockInModal: React.FC = ({ return; } + const isJobOrderSource = Boolean(stockInLineInfo?.jobOrderId) || printSource === "productionProcess"; const qcData = { dnNo : data.dnNo? data.dnNo : "DN00000", // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), @@ -413,6 +414,9 @@ const QcStockInModal: React.FC = ({ qcAccept: qcAccept? qcAccept : false, acceptQty: acceptQty? acceptQty : 0, + // For Job Order QC, allow updating received qty beyond demand/accepted. + // Backend uses request.acceptedQty in QC flow, so we must send it explicitly. + acceptedQty: (qcAccept && isJobOrderSource) ? (acceptQty ? acceptQty : 0) : stockInLineInfo?.acceptedQty, // qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ qcResult: qcResults.map(item => ({ // id: item.id, @@ -481,8 +485,8 @@ const QcStockInModal: React.FC = ({ itemId: stockInLineInfo?.itemId, // Include Item ID purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL ID if exists - acceptedQty:acceptQty, // Include acceptedQty - acceptQty: stockInLineInfo?.acceptedQty, // Putaway quantity + acceptedQty: acceptQty, // Keep in sync with QC acceptQty + acceptQty: acceptQty, // Putaway quantity warehouseId: defaultWarehouseId, status: "received", // Use string like PutAwayModal productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined, @@ -490,7 +494,7 @@ const QcStockInModal: React.FC = ({ receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined, inventoryLotLines: [{ warehouseId: defaultWarehouseId, - qty: stockInLineInfo?.acceptedQty, // Simplified like PutAwayModal + qty: acceptQty, // Putaway qty }], } as StockInLineEntry & ModalFormInput; diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 73cad78..a254ecb 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -10,6 +10,8 @@ "Issue BOM List": "問題 BOM 列表", "File Name": "檔案名稱", "Please Select BOM": "請選擇 BOM", + "No Lot": "沒有批號", + "Select All": "全選", "Loading BOM Detail...": "正在載入 BOM 明細…", "Output Quantity": "使用數量", "Process & Equipment": "製程與設備",