From 081ccb9f8f09efb9e30961547f8991ee56ae72e5 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sat, 21 Mar 2026 22:41:50 +0800 Subject: [PATCH] update job order search and cacel job order --- src/app/api/jo/actions.ts | 11 +++ src/components/JoSearch/JoSearch.tsx | 89 +++++++++++++++-- src/components/JoSearch/JoSearchWrapper.tsx | 1 + .../OverallTimeRemainingCard.tsx | 11 ++- .../ProductionProcessJobOrderDetail.tsx | 96 +++++++++++++++---- .../ProductionProcessList.tsx | 28 ++---- src/i18n/zh/common.json | 5 + src/i18n/zh/jo.json | 8 ++ 8 files changed, 205 insertions(+), 44 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index ca5bdad..97cbe0e 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -29,6 +29,7 @@ export interface SearchJoResultRequest extends Pageable { planStart?: string; planStartTo?: string; jobTypeName?: string; + joSearchStatus?: string; } export interface productProcessLineQtyRequest { @@ -672,6 +673,16 @@ export const deleteJobOrder=cache(async (jobOrderId: number) => { } ); }); + +export const setJobOrderHidden = cache(async (jobOrderId: number, hidden: boolean) => { + const response = await serverFetchJson(`${BASE_API_URL}/jo/set-hidden`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: jobOrderId, hidden }), + }); + revalidateTag("jos"); + return response; +}); export const fetchAllJobTypes = cache(async () => { return serverFetchJson( `${BASE_API_URL}/jo/jobTypes`, diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index ed34f30..580887f 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -1,5 +1,5 @@ "use client" -import { SearchJoResultRequest, fetchJos, releaseJo, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; +import { SearchJoResultRequest, fetchJos, releaseJo, setJobOrderHidden, 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"; @@ -39,8 +39,7 @@ interface Props { jobTypes: JobTypeResponse[]; } -type SearchQuery = Partial>; -type SearchParamNames = keyof SearchQuery; +type SearchParamNames = "code" | "itemName" | "planStart" | "planStartTo" | "jobTypeName" | "joSearchStatus"; const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => { const { t } = useTranslation("jo"); @@ -58,6 +57,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]); const [releasingJoIds, setReleasingJoIds] = useState>(new Set()); const [isBatchReleasing, setIsBatchReleasing] = useState(false); + const [cancelConfirmJoId, setCancelConfirmJoId] = useState(null); + const [cancelSubmitting, setCancelSubmitting] = useState(false); + const [cancelingJoIds, setCancelingJoIds] = useState>(new Set()); // 合并后的统一编辑 Dialog 状态 const [openEditDialog, setOpenEditDialog] = useState(false); @@ -160,6 +162,19 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT type: "select", options: jobTypes.map(jt => jt.name) }, + { + label: t("Status"), + paramName: "joSearchStatus", + type: "select-labelled", + options: [ + { label: t("Pending"), value: "pending" }, + { label: t("Packaging"), value: "packaging" }, + { label: t("Processing"), value: "processing" }, + { label: t("Storing"), value: "storing" }, + { label: t("Put Awayed"), value: "putAwayed" }, + { label: t("cancel"), value: "cancel" }, + ], + }, ], [t, jobTypes]) const fetchBomForJo = useCallback(async (jo: JobOrder): Promise => { @@ -288,6 +303,29 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT }); } }, [inputs, pagingController, t, newPageFetch]); + + const handleConfirmCancelJobOrder = useCallback(async () => { + if (cancelConfirmJoId == null) return; + const id = cancelConfirmJoId; + setCancelSubmitting(true); + setCancelingJoIds((prev) => new Set(prev).add(id)); + try { + await setJobOrderHidden(id, true); + msg(t("update success")); + setCancelConfirmJoId(null); + await newPageFetch(pagingController, inputs); + } catch (error) { + console.error("Error cancelling job order:", error); + msg(t("update failed")); + } finally { + setCancelSubmitting(false); + setCancelingJoIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + } + }, [cancelConfirmJoId, newPageFetch, pagingController, inputs, t]); const selectedPlanningJoIds = useMemo(() => { const selectedIds = new Set(checkboxIds.map((id) => Number(id))); @@ -444,6 +482,8 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT renderCell: (row) => { const isPlanning = isPlanningJo(row); const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing; + const isCancelingRow = cancelingJoIds.has(row.id); + const isPutAwayed = row.stockInLineStatus?.toLowerCase() === "completed"; return ( + {isPlanning ? ( + ) : ( + + )} ) } }, - ], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing] + ], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing, cancelingJoIds, cancelSubmitting] ) const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { @@ -622,7 +679,8 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT ...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 : "" + jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "", + joSearchStatus: query.joSearchStatus && query.joSearchStatus !== "All" ? query.joSearchStatus : "all", }; setInputs({ @@ -630,7 +688,8 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT itemName: transformedQuery.itemName, planStart: transformedQuery.planStart, planStartTo: transformedQuery.planStartTo, - jobTypeName: transformedQuery.jobTypeName + jobTypeName: transformedQuery.jobTypeName, + joSearchStatus: transformedQuery.joSearchStatus }); setPagingController(defaultPagingController); @@ -839,6 +898,24 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT + + !cancelSubmitting && setCancelConfirmJoId(null)} + maxWidth="xs" + fullWidth + > + {t("Confirm cancel job order")} + + {t("Cancel job order confirm message")} + + + + + + } diff --git a/src/components/JoSearch/JoSearchWrapper.tsx b/src/components/JoSearch/JoSearchWrapper.tsx index 68e894b..e420886 100644 --- a/src/components/JoSearch/JoSearchWrapper.tsx +++ b/src/components/JoSearch/JoSearchWrapper.tsx @@ -18,6 +18,7 @@ const JoSearchWrapper: React.FC & SubComponents = async () => { itemName: "", planStart: `${todayStr}T00:00`, planStartTo: `${todayStr}T23:59:59`, + joSearchStatus: "all", } diff --git a/src/components/ProductionProcess/OverallTimeRemainingCard.tsx b/src/components/ProductionProcess/OverallTimeRemainingCard.tsx index 7b06f68..9d9261d 100644 --- a/src/components/ProductionProcess/OverallTimeRemainingCard.tsx +++ b/src/components/ProductionProcess/OverallTimeRemainingCard.tsx @@ -22,7 +22,14 @@ const OverallTimeRemainingCard: React.FC = ({ console.log("🕐 OverallTimeRemainingCard - processData?.startTime type:", typeof processData?.startTime); console.log("🕐 OverallTimeRemainingCard - processData?.startTime isArray:", Array.isArray(processData?.startTime)); - if (!processData?.startTime) { + const jobOrderStatus = String((processData as any)?.jobOrderStatus ?? "").trim().toLowerCase(); + const shouldStopCount = + jobOrderStatus === "storing" || + jobOrderStatus === "completed" || + jobOrderStatus === "pendingqc" || + jobOrderStatus === "pending_qc"; + + if (shouldStopCount || !processData?.startTime) { console.log("❌ OverallTimeRemainingCard - No startTime found"); setOverallRemainingTime(null); setIsOverTime(false); @@ -176,7 +183,7 @@ const OverallTimeRemainingCard: React.FC = ({ update(); const timer = setInterval(update, 1000); return () => clearInterval(timer); - }, [processData?.startTime, processData?.productProcessLines]); + }, [processData?.startTime, processData?.productProcessLines, (processData as any)?.jobOrderStatus]); if (!processData?.startTime || overallRemainingTime === null) { return null; diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index c69bb96..ddd0852 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -23,7 +23,7 @@ import { } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; -import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions"; +import { fetchProductProcessesByJobOrderId ,deleteJobOrder, setJobOrderHidden, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine,JobOrderLineInfo} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; import { BomCombo } from "@/app/api/bom"; import { fetchBomCombo } from "@/app/api/bom/index"; @@ -265,15 +265,42 @@ const stockCounts = useMemo(() => { insufficient: total - sufficient, }; }, [jobOrderLines, inventoryData]); -const status = processData?.status?.toLowerCase?.() ?? ""; -const handleDeleteJobOrder = useCallback(async ( jobOrderId: number) => { - const response = await deleteJobOrder(jobOrderId) - if (response) { - //setProcessData(response.entity); - //await fetchData(); +const jobOrderPlanning = useMemo( + () => (processData?.jobOrderStatus ?? "").toLowerCase() === "planning", + [processData?.jobOrderStatus] +); +const isPutAwayed = useMemo( + () => (processData?.jobOrderStatus ?? "").toLowerCase() === "completed", + [processData?.jobOrderStatus] +); +const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); +const [cancelConfirmOpen, setCancelConfirmOpen] = useState(false); +const [deleteLoading, setDeleteLoading] = useState(false); +const [cancelLoading, setCancelLoading] = useState(false); + +const handleConfirmDeleteJobOrder = useCallback(async () => { + setDeleteLoading(true); + try { + const response = await deleteJobOrder(jobOrderId); + if (response) { + setDeleteConfirmOpen(false); + onBack(); + } + } finally { + setDeleteLoading(false); + } +}, [jobOrderId, onBack]); + +const handleConfirmCancelJobOrder = useCallback(async () => { + setCancelLoading(true); + try { + await setJobOrderHidden(jobOrderId, true); + setCancelConfirmOpen(false); onBack(); + } finally { + setCancelLoading(false); } -}, [jobOrderId]); +}, [jobOrderId, onBack]); const handleRelease = useCallback(async ( jobOrderId: number) => { // TODO: 替换为实际的 release 调用 console.log("Release clicked for jobOrderId:", jobOrderId); @@ -675,15 +702,24 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { {t("Lines with insufficient stock: ")}{stockCounts.insufficient} - {fromJosave && ( + {fromJosave && jobOrderPlanning && ( + variant="contained" + color="error" + onClick={() => setDeleteConfirmOpen(true)} + > + {t("Delete Job Order")} + + )} + {fromJosave && !jobOrderPlanning && ( + )} {fromJosave && ( + + + + + !cancelLoading && setCancelConfirmOpen(false)} maxWidth="xs" fullWidth> + {t("Confirm cancel job order")} + + {t("Cancel job order confirm message")} + + + + + + + diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 0fb150e..ab5bf35 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -217,16 +217,11 @@ const ProductProcessList: React.FC = ({ (inputs: Record) => { const selectedProcessType = (inputs.processType || "all") as ProcessFilter; const fallback = defaultPlanStartRange(); - let from = (inputs.date || "").trim() || fallback.from; - let to = (inputs.dateTo || "").trim() || fallback.to; - if (dayjs(from).isAfter(dayjs(to), "day")) { - [from, to] = [to, from]; - } + const selectedDate = (inputs.date || "").trim() || fallback.from; onListPersistedStateChange((prev) => ({ ...prev, filter: selectedProcessType, - planStartFrom: from, - planStartTo: to, + date: selectedDate, itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null, jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null, selectedItemCodes: [], @@ -241,8 +236,7 @@ const ProductProcessList: React.FC = ({ onListPersistedStateChange((prev) => ({ ...prev, filter: "all", - planStartFrom: r.from, - planStartTo: r.to, + date: r.from, itemCode: null, jobOrderCode: null, selectedItemCodes: [], @@ -368,16 +362,11 @@ const ProductProcessList: React.FC = ({ const r = defaultPlanStartRange(); return [ { - type: "dateRange", - label: t("Plan start (from)"), - label2: t("Plan start (to)"), + type: "date", + label: t("Search date"), paramName: "date", - defaultValue: r.from, - defaultValueTo: r.to, - preFilledValue: { - from: appliedSearch.date, - to: appliedSearch.date, - }, + defaultValue: appliedSearch.date, + preFilledValue: appliedSearch.date, }, { type: "text", @@ -471,6 +460,7 @@ const ProductProcessList: React.FC = ({ {paged.map((process) => { const status = String(process.status || ""); const statusLower = status.toLowerCase(); + const displayStatus = statusLower === "in_progress" ? "processing" : status; const statusColor = statusLower === "completed" ? "success" @@ -529,7 +519,7 @@ const ProductProcessList: React.FC = ({ - + {t("Lot No")}: {process.lotNo ?? "-"} diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index ce0ff79..82c6bfc 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -24,6 +24,11 @@ "Confirm to Pass this Process?": "確認要通過此工序嗎?", "Equipment Name": "設備", "Confirm to update this Job Order?": "確認要完成此工單嗎?", + "Cancel Job Order": "取消工單", + "Confirm delete job order": "確認刪除工單", + "Delete job order confirm message": "確定要刪除此工單嗎?此操作無法復原。", + "Confirm cancel job order": "確認取消工單", + "Cancel job order confirm message": "確定要取消此工單嗎?工單將從列表中隱藏。", "all": "全部", "Bom Uom": "BOM 單位", "Searched Item": "已搜索物料", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 23e8fa5..730bad0 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -25,6 +25,7 @@ "UoM": "銷售單位", "Select Another Bag Lot":"選擇另一個包裝袋", "No": "沒有", + "Packaging":"提料中", "Overall Time Remaining": "總剩餘時間", "User not found with staffNo:": "用戶不存在", "Time Remaining": "剩餘時間", @@ -41,9 +42,16 @@ "Lot No.": "批號", "Pass": "通過", "Delete Job Order": "刪除工單", + "Cancel Job Order": "取消工單", + "Confirm delete job order": "確認刪除工單", + "Delete job order confirm message": "確定要刪除此工單嗎?此操作無法復原。", + "Confirm cancel job order": "確認取消工單", + "Cancel job order confirm message": "確定要取消此工單嗎?工單將從列表中隱藏。", "Bom": "半成品/成品編號", "Release": "放單", "Pending": "待掃碼", + "Put Awayed": "已上架", + "cancel": "已取消", "Pending for pick": "待提料", "Planning": "計劃中", "Processing": "已開始工序",