From cbe50272e2910106039bdea035c6b293030626bd Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 14 Oct 2026 12:15:41 +0800 Subject: [PATCH] stock take block any other in/out while stock taking --- .../InventorySearch/InventoryLotLineTable.tsx | 32 +++- src/components/Qc/QcStockInModal.tsx | 19 ++- .../ApproverStockTakeAll.tsx | 146 ++++++++++-------- .../StockTakeManagement/PickerCardList.tsx | 5 +- .../StockTakeRoundCardList.tsx | 65 ++++++-- .../StockTakeManagement/StockTakeTab.tsx | 2 +- src/components/Swal/CustomAlerts.tsx | 2 + src/components/Swal/sweetalert2.css | 18 +++ src/i18n/zh/stockTake.json | 4 +- 9 files changed, 209 insertions(+), 84 deletions(-) diff --git a/src/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx index 874c32c5..a3ca3e1c 100644 --- a/src/components/InventorySearch/InventoryLotLineTable.tsx +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -36,6 +36,31 @@ import { submitStockAdjustment, StockAdjustmentLineRequest, fetchLatestAdjustmen 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; +} + type AdjustmentEntry = InventoryLotLineResult & { adjustedQty: number; originalQty?: number; @@ -571,9 +596,12 @@ const prevAdjustmentModalOpenRef = useRef(false); } else { throw new Error(response?.message || t("Failed to transfer stock")); } - } catch (error: any) { + } catch (error: unknown) { console.error("Error transferring stock:", error); - msgError(error?.message || t("Failed to transfer stock. Please try again.")); + msgError(readableServerMessage(error, t("Failed to transfer stock. Please try again.")), { + wrap: true, + timer: 6000, + }); } finally { setIsUploading(false); } diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index b38d37dd..818e2795 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -573,7 +573,9 @@ const QcStockInModal: React.FC = ({ try { // Use updateStockInLine directly like PutAwayModal does const res = await updateStockInLine(putawayData); - if (Boolean(res.id)) { + if (res?.type === "putaway_held") { + alert(res.message || "盤點進行中,自動上架將於盤點結束後入庫"); + } else if (Boolean(res.id)) { console.log("Auto putaway completed for job order bom"); } } catch (error) { @@ -664,9 +666,18 @@ const QcStockInModal: React.FC = ({ return; } // Handle putaway submission logic here - const res = await postStockInLine(putawayData); - console.log("result ", res); - + try { + const res = await postStockInLine(putawayData); + console.log("result ", res); + if (res?.type === "putaway_held") { + alert(res.message || "盤點進行中,自動上架將於盤點結束後入庫"); + } + } catch (error) { + const message = error instanceof Error ? error.message : "上架失敗"; + alert(message); + return; + } + // Close modal after successful putaway closeHandler({}, "backdropClick"); }, diff --git a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx index ddb927ad..ec60c86f 100644 --- a/src/components/StockTakeManagement/ApproverStockTakeAll.tsx +++ b/src/components/StockTakeManagement/ApproverStockTakeAll.tsx @@ -1638,6 +1638,49 @@ const ApproverStockTakeAll: React.FC = ({ ) : null; const batchButtonInTabBar = Boolean(batchButtonHost && batchSaveButton); + const searchFieldCount = + (searchKind === "section" ? 2 : 0) + 1 + (mode === "pending" ? 1 : 0) + 1; + const varianceColumn = ((searchFieldCount - 1) % 3) + 1; + const rowFillers = searchFieldCount % 3 === 0 ? 0 : 3 - (searchFieldCount % 3); + const varianceFilterControl = ( + <> + { + setSearchVarianceFilterInclusive(e.target.checked); + handleSearch({ varianceFilterInclusive: e.target.checked }); + }} + /> + } + label={{t("Variance filter inclusive only")}} + /> + + {searchVarianceFilterInclusive + ? t("Variance filter inclusive range hint", { + value: searchVariancePercentTolerance || "0", + op: searchVarianceFilterStrict ? "<" : "≤", + }) + : t("Variance filter exclusive range hint", { + value: searchVariancePercentTolerance || "0", + op: searchVarianceFilterStrict ? ">" : "≥", + })} + + + ); + const searchActionButtons = ( + + + + + ); return ( @@ -1984,72 +2027,53 @@ const ApproverStockTakeAll: React.FC = ({ {t("Pass")} - - - - )} - - { - const next = sanitizePositiveDecimalInput(e.target.value); - setSearchVariancePercentTolerance(next); - handleSearch({ variancePercentTolerance: next }); - }} - inputProps={{ min: 0, step: 0.1 }} - sx={approverSearchFieldSx} - /> - { - setSearchVarianceFilterInclusive(e.target.checked); - handleSearch({ varianceFilterInclusive: e.target.checked }); - }} - /> - } - label={ - {t("Variance filter inclusive only")} - } - /> - - {searchVarianceFilterInclusive - ? t("Variance filter inclusive range hint", { - value: searchVariancePercentTolerance || "0", - op: searchVarianceFilterStrict ? "<" : "≤", - }) - : t("Variance filter exclusive range hint", { - value: searchVariancePercentTolerance || "0", - op: searchVarianceFilterStrict ? ">" : "≥", - })} - - {mode !== "pending" && ( - - - - - )} - + { + const next = sanitizePositiveDecimalInput(e.target.value); + setSearchVariancePercentTolerance(next); + handleSearch({ variancePercentTolerance: next }); + }} + inputProps={{ min: 0, step: 0.1 }} + sx={approverSearchFieldSx} + /> + {Array.from({ length: rowFillers }).map((_, index) => ( + + ))} + {varianceColumn === 1 ? ( + <> + + {varianceFilterControl} + + {searchActionButtons} + + ) : ( + <> + + {searchActionButtons} + + {varianceColumn === 3 ? ( + + ) : null} + + {varianceFilterControl} + + + )} diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index 17c13122..64a9e37f 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -745,6 +745,7 @@ const PickerCardList: React.FC = ({ const handleOpenCreateStockTakeSummaryConfirm = useCallback(() => { if (createDialogPlanStart == null || !createDialogPlanStart.isValid()) return; + if (createDialogPlanStart.startOf("day").isBefore(dayjs().startOf("day"))) return; if (createMode === "item") { if (createDialogSelectedItems.length === 0) return; } else if (createDialogSelectedSections.length === 0) { @@ -1803,6 +1804,7 @@ const PickerCardList: React.FC = ({ value={createDialogPlanStart} onChange={(newValue) => setCreateDialogPlanStart(newValue)} format={OUTPUT_DATE_FORMAT} + minDate={dayjs().startOf("day")} slotProps={{ textField: { fullWidth: true, @@ -2239,7 +2241,8 @@ const PickerCardList: React.FC = ({ ? createDialogSelectedItems.length === 0 : createDialogSelectedSections.length === 0) || createDialogPlanStart == null || - !createDialogPlanStart.isValid() + !createDialogPlanStart.isValid() || + createDialogPlanStart.startOf("day").isBefore(dayjs().startOf("day")) } > {t("Confirm")} diff --git a/src/components/StockTakeManagement/StockTakeRoundCardList.tsx b/src/components/StockTakeManagement/StockTakeRoundCardList.tsx index a1e41e9f..210e1f6b 100644 --- a/src/components/StockTakeManagement/StockTakeRoundCardList.tsx +++ b/src/components/StockTakeManagement/StockTakeRoundCardList.tsx @@ -4,6 +4,9 @@ import { Box, Button, Card, + FormControl, + MenuItem, + Select, CardActions, CardContent, Chip, @@ -72,7 +75,7 @@ const StockTakeRoundCardList: React.FC = ({ yearMonth, onYearMonthChange, }) => { - const { t } = useTranslation(["stockTake", "common"]); + const { t, i18n } = useTranslation(["stockTake", "common"]); const [loading, setLoading] = useState(true); const [hasOpenRounds, setHasOpenRounds] = useState(false); const [rounds, setRounds] = useState([]); @@ -103,11 +106,19 @@ const StockTakeRoundCardList: React.FC = ({ void load(yearMonth); }, [load, yearMonth, refreshNonce]); - // Completed months plus current month (even if no round created yet). No future months. + // Every month from the earliest round through this month, including months with no round. const monthOptions = useMemo(() => { - const set = new Set(completedYearMonths.filter((m) => m && m <= thisMonth)); - set.add(thisMonth); - return Array.from(set).sort((a, b) => b.localeCompare(a)); + const known = completedYearMonths.filter((m) => m && m <= thisMonth); + const earliest = known.reduce((a, b) => (a < b ? a : b), thisMonth); + const months: string[] = []; + let cursor = dayjs(`${earliest}-01`); + const end = dayjs(`${thisMonth}-01`); + if (!cursor.isValid() || !end.isValid()) return [thisMonth]; + while (cursor.isBefore(end) || cursor.isSame(end, "month")) { + months.push(cursor.format("YYYY-MM")); + cursor = cursor.add(1, "month"); + } + return months.sort((a, b) => b.localeCompare(a)); }, [completedYearMonths, thisMonth]); const monthIndex = useMemo(() => { @@ -115,6 +126,16 @@ const StockTakeRoundCardList: React.FC = ({ return i >= 0 ? i : 0; }, [monthOptions, yearMonth]); + const formatYearMonth = (ym: string) => { + const parsed = dayjs(`${ym}-01`); + if (!parsed.isValid()) return ym; + return i18n.language?.toLowerCase().startsWith("zh") + ? parsed.format("YYYY年M月") + : parsed.format("MMM YYYY"); + }; + const olderMonth = monthOptions[monthIndex + 1]; + const newerMonth = monthIndex > 0 ? monthOptions[monthIndex - 1] : undefined; + const goMonth = (delta: number) => { if (hasOpenRounds || monthOptions.length === 0) return; const next = monthIndex + delta; @@ -157,7 +178,7 @@ const StockTakeRoundCardList: React.FC = ({ {hasOpenRounds ? t("Open rounds this month") - : t("Completed rounds for month", { month: yearMonth })} + : t("Completed rounds for month", { month: formatYearMonth(yearMonth) })} {!hasOpenRounds && monthOptions.length > 0 ? ( @@ -165,16 +186,34 @@ const StockTakeRoundCardList: React.FC = ({ - - {yearMonth} - - ) : null} diff --git a/src/components/StockTakeManagement/StockTakeTab.tsx b/src/components/StockTakeManagement/StockTakeTab.tsx index 6f77123e..7b5fc06e 100644 --- a/src/components/StockTakeManagement/StockTakeTab.tsx +++ b/src/components/StockTakeManagement/StockTakeTab.tsx @@ -229,7 +229,7 @@ const StockTakeTab: React.FC = () => { ) ) : null} - {isAdmin && ( + {isAdmin && selectedRound && (tabValue === 1 || tabValue === 2) && ( { @@ -57,6 +58,7 @@ export const msgError = (title: SweetAlertTitle, options?: MsgOptions) => { }, customClass: { container: 'swal2-custom-zindex', + popup: options?.wrap ? "swal2-toast-wrap" : undefined, }, }).fire({ icon: "error", diff --git a/src/components/Swal/sweetalert2.css b/src/components/Swal/sweetalert2.css index ac7225fa..88adb18f 100644 --- a/src/components/Swal/sweetalert2.css +++ b/src/components/Swal/sweetalert2.css @@ -5,3 +5,21 @@ .swal-popup-class { z-index: 10000; /* Adjust the z-index value as needed */ } + +.swal2-popup.swal2-toast.swal2-toast-wrap { + box-sizing: border-box !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + overflow: hidden !important; + grid-template-columns: auto minmax(0, 1fr) !important; +} +.swal2-popup.swal2-toast.swal2-toast-wrap .swal2-title { + min-width: 0 !important; + white-space: normal !important; + overflow: visible !important; + text-overflow: unset !important; + max-height: none !important; + line-height: 1.4 !important; + word-break: break-word !important; +} diff --git a/src/i18n/zh/stockTake.json b/src/i18n/zh/stockTake.json index 633f14f4..01eada92 100644 --- a/src/i18n/zh/stockTake.json +++ b/src/i18n/zh/stockTake.json @@ -11,8 +11,8 @@ "Previous month": "上一個月", "Round closed": "已關閉此輪", "Main floor": "主要樓層", - "Stock taken": "已盤點", - "Approved progress": "已審核", + "Stock taken": "已盤點區域", + "Approved progress": "已審核區域", "Stock taken lots": "已盤點批次", "Approved lots": "已審核批次", "Stock Take Section": "盤點區域類型",