From 29c350ccac2d6db72527337be82111ac5ef30386 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 20 Oct 2026 15:47:54 +0800 Subject: [PATCH] stock take block, created round created again hide another sldie create --- src/app/utils/readableServerMessage.ts | 26 ++++++ .../InventorySearch/InventoryLotLineTable.tsx | 29 +------ src/components/PoDetail/QrModal.tsx | 15 +++- src/components/PutAwayScan/PutAwayModal.tsx | 16 +++- .../StockTakeManagement/PickerCardList.tsx | 86 ++++++++++++++++--- .../StockTakeManagement/StockTakeTab.tsx | 2 +- src/i18n/en/stockTake.json | 1 + src/i18n/zh/stockTake.json | 1 + 8 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 src/app/utils/readableServerMessage.ts diff --git a/src/app/utils/readableServerMessage.ts b/src/app/utils/readableServerMessage.ts new file mode 100644 index 00000000..1a7f81c8 --- /dev/null +++ b/src/app/utils/readableServerMessage.ts @@ -0,0 +1,26 @@ +/** Prefer stock-take / API reason over a generic "Server error: 409 …" wrapper. */ +export function readableServerMessage(error: unknown, fallback: string): string { + const raw = + error instanceof Error ? error.message : typeof error === "string" ? error : ""; + const sentence = raw.match(/盤點進行中,(?:區域\s+\S+|貨品\s+.+?)\s*暫停出入庫/); + if (sentence) return sentence[0]; + // Spring CONFLICT often puts the reason in "error" (not "message"). + const fieldPattern = /"(?:message|detail|error)"\s*:\s*"((?:\\.|[^"\\])*)"/g; + const fields: string[] = []; + let match: RegExpExecArray | null = fieldPattern.exec(raw); + while (match) { + if (match[1]) fields.push(match[1]); + match = fieldPattern.exec(raw); + } + for (let i = fields.length - 1; i >= 0; i -= 1) { + const text = fields[i] + .replace(/\\"/g, '"') + .replace(/\\n/g, " ") + .replace(/\\u([0-9a-fA-F]{4})/g, (_full: string, hex: string) => + String.fromCharCode(parseInt(hex, 16)), + ) + .trim(); + if (text && !text.startsWith("Server error") && !text.startsWith("{")) return text; + } + return fallback; +} diff --git a/src/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx index a3ca3e1c..b68ecd23 100644 --- a/src/components/InventorySearch/InventoryLotLineTable.tsx +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -35,31 +35,7 @@ import CheckIcon from "@mui/icons-material/Check"; import { submitStockAdjustment, StockAdjustmentLineRequest, fetchLatestAdjustmentRemarks } from "@/app/api/stockAdjustment/actions"; import { useSession } from "next-auth/react"; import { AUTH, hasAbility } from "@/authorities"; - -function readableServerMessage(error: unknown, fallback: string) { - const raw = - error instanceof Error ? error.message : typeof error === "string" ? error : ""; - const sentence = raw.match(/盤點進行中,(?:區域\s+\S+|貨品\s+.+?)\s*暫停出入庫/); - if (sentence) return sentence[0]; - const fieldPattern = /"(?:message|detail)"\s*:\s*"((?:\\.|[^"\\])*)"/g; - const fields: string[] = []; - let match: RegExpExecArray | null = fieldPattern.exec(raw); - while (match) { - if (match[1]) fields.push(match[1]); - match = fieldPattern.exec(raw); - } - for (let i = fields.length - 1; i >= 0; i -= 1) { - const text = fields[i] - .replace(/\\"/g, '"') - .replace(/\\n/g, " ") - .replace(/\\u([0-9a-fA-F]{4})/g, (_full: string, hex: string) => - String.fromCharCode(parseInt(hex, 16)), - ) - .trim(); - if (text && !text.startsWith("Server error") && !text.startsWith("{")) return text; - } - return fallback; -} +import { readableServerMessage } from "@/app/utils/readableServerMessage"; type AdjustmentEntry = InventoryLotLineResult & { adjustedQty: number; @@ -331,8 +307,7 @@ const prevAdjustmentModalOpenRef = useRef(false); setStockAdjustmentModalOpen(false); await onStockAdjustmentSuccess?.(); } catch (e: unknown) { - const message = e instanceof Error ? e.message : String(e); - msgError(message || t("Save failed")); + msgError(readableServerMessage(e, t("Save failed"))); } finally { setIsUploading(false); setIsAdjustSaving(false); diff --git a/src/components/PoDetail/QrModal.tsx b/src/components/PoDetail/QrModal.tsx index d5f32688..eeb08568 100644 --- a/src/components/PoDetail/QrModal.tsx +++ b/src/components/PoDetail/QrModal.tsx @@ -29,6 +29,7 @@ import { Check } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "next/navigation"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; +import { readableServerMessage } from "@/app/utils/readableServerMessage"; interface Props extends Omit { warehouse: WarehouseResult[]; @@ -173,6 +174,12 @@ const QrModal: React.FC = ({ open, onClose, warehouse }) => { } // return; const res = await updateStockInLine(args); + if (res?.type === "putaway_held" || res?.code === "PUTAWAY_HELD") { + setServerError( + res.message || "盤點進行中,自動上架將於盤點結束後入庫", + ); + return; + } if (Boolean(res.id)) { // update entries console.log(res); @@ -182,8 +189,12 @@ const QrModal: React.FC = ({ open, onClose, warehouse }) => { console.log(res); // if (res) } catch (e) { - // server error - setServerError(t("An error has occurred. Please try again later.")); + setServerError( + readableServerMessage( + e, + t("An error has occurred. Please try again later."), + ), + ); console.log(e); } }, diff --git a/src/components/PutAwayScan/PutAwayModal.tsx b/src/components/PutAwayScan/PutAwayModal.tsx index 1cfdaac8..9e495f7b 100644 --- a/src/components/PutAwayScan/PutAwayModal.tsx +++ b/src/components/PutAwayScan/PutAwayModal.tsx @@ -39,6 +39,7 @@ import { QrCodeScanner } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { msg, msgError } from "../Swal/CustomAlerts"; import { PutAwayRecord } from "."; import FgStockInForm from "../StockIn/FgStockInForm"; +import { readableServerMessage } from "@/app/utils/readableServerMessage"; import Swal from "sweetalert2"; @@ -449,6 +450,13 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId } console.log("%c Submitting Data:", "color:blue", args); const res = await updateStockInLine(args); + if (res?.type === "putaway_held" || res?.code === "PUTAWAY_HELD") { + const heldMsg = + res.message || "盤點進行中,自動上架將於盤點結束後入庫"; + setServerError(heldMsg); + msgError(heldMsg); + return; + } if (Boolean(res.id)) { // update entries console.log("%c Update Success:", "color:green", res); @@ -475,8 +483,12 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId // console.log(res); // if (res) } catch (e) { - // server error - setServerError(t("An error has occurred. Please try again later.")); + const message = readableServerMessage( + e, + t("An error has occurred. Please try again later."), + ); + setServerError(message); + msgError(message); console.log(e); } }, diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index 64a9e37f..84d3ee7d 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -32,8 +32,12 @@ import { ToggleButton, ToggleButtonGroup, Checkbox, + Accordion, + AccordionSummary, + AccordionDetails, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined"; import { useRouter } from "next/navigation"; import { SessionWithTokens } from "@/config/authConfig"; @@ -318,6 +322,8 @@ const PickerCardList: React.FC = ({ dayjs.extend(duration); const [loading, setLoading] = useState(true); + /** Same UX as Approver: Search Criteria accordion can collapse / expand. */ + const [showFilters, setShowFilters] = useState(true); /** 這一輪全部卡片,篩選在前端做,點選不再重打 API */ const [roundRecords, setRoundRecords] = useState([]); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -342,7 +348,9 @@ const PickerCardList: React.FC = ({ const [createFloorTabIndex, setCreateFloorTabIndex] = useState(0); const [createDialogSearchQuery, setCreateDialogSearchQuery] = useState(""); const [createMode, setCreateMode] = useState<"section" | "item">("section"); + /** Open item round → hide 「依區域」; open section round → hide 「依貨品」 (symmetric). */ const [blockSectionCreate, setBlockSectionCreate] = useState(false); + const [blockItemCreate, setBlockItemCreate] = useState(false); const [createDialogItemCode, setCreateDialogItemCode] = useState(""); const [createDialogItemName, setCreateDialogItemName] = useState(""); const [createDialogItemHits, setCreateDialogItemHits] = useState([]); @@ -718,15 +726,28 @@ const PickerCardList: React.FC = ({ getStockTakeRoundCards() .then((res) => { if (cancelled) return; - const blocked = (res.rounds || []).some( - (round) => round.open && round.stockTakeRoundMode === "item", + const openRounds = res.rounds || []; + const blockSection = openRounds.some( + (round) => + round.open && + String(round.stockTakeRoundMode ?? "").trim().toLowerCase() === "item", ); - setBlockSectionCreate(blocked); - if (blocked) setCreateMode("item"); + const blockItem = openRounds.some((round) => { + if (!round.open) return false; + const mode = String(round.stockTakeRoundMode ?? "section").trim().toLowerCase(); + return mode === "section" || mode === ""; + }); + setBlockSectionCreate(blockSection); + setBlockItemCreate(blockItem); + if (!blockSection) setCreateMode("section"); + else if (!blockItem) setCreateMode("item"); }) .catch((e) => { - console.error("Failed to check open item stock take rounds:", e); - if (!cancelled) setBlockSectionCreate(false); + console.error("Failed to check open stock take create modes:", e); + if (!cancelled) { + setBlockSectionCreate(false); + setBlockItemCreate(false); + } }); return () => { cancelled = true; @@ -1070,7 +1091,7 @@ const PickerCardList: React.FC = ({ setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); setCreateDialogSelectedItems([]); - setCreateMode("section"); + setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section"); } catch (e) { console.error(e); } finally { @@ -1078,6 +1099,8 @@ const PickerCardList: React.FC = ({ createStockTakeInFlightRef.current = false; } }, [ + blockItemCreate, + blockSectionCreate, createDialogPlanStart, createDialogRoundName, createDialogSelectedItems, @@ -1235,7 +1258,7 @@ const PickerCardList: React.FC = ({ setOpenConfirmDialog(false); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); - setCreateMode(blockSectionCreate ? "item" : "section"); + setCreateMode(blockSectionCreate && !blockItemCreate ? "item" : "section"); setCreateDialogItemCode(""); setCreateDialogItemName(""); setCreateDialogItemHits([]); @@ -1246,11 +1269,28 @@ const PickerCardList: React.FC = ({ {!hideSectionList && ( <> - - - - {t("Search Criteria")} + setShowFilters(expanded)} + elevation={0} + sx={{ mb: 2, border: "1px solid", borderColor: "divider", borderRadius: 1 }} + > + } + sx={{ + minHeight: 36, + px: 2, + "&.Mui-expanded": { minHeight: 36 }, + "& .MuiAccordionSummary-content, & .MuiAccordionSummary-content.Mui-expanded": { my: 0.75 }, + }} + > + + {t("Search Criteria")} + + + + = ({ + + @@ -1728,18 +1770,34 @@ const PickerCardList: React.FC = ({ + {blockSectionCreate && blockItemCreate ? ( + + {t("Cannot create while both section and item rounds are open")} + + ) : ( { if (!v) return; + if (v === "section" && blockSectionCreate) return; + if (v === "item" && blockItemCreate) return; setCreateMode(v); }} > {blockSectionCreate ? null : ( )} - + {blockItemCreate ? null : ( + + )} + )} { ) ) : null} - {isAdmin && selectedRound && (tabValue === 1 || tabValue === 2) && ( + {isAdmin && selectedRound && tabValue === 1 && (