From 79a4f8564651e58683c1a08dd16af8d90147bac2 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 8 Sep 2026 22:22:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=9F=E5=93=81=E5=85=A9=E5=80=8B=20Excel=20?= =?UTF-8?q?=E6=8C=89=E9=88=95=EF=BC=8F=E5=88=86=E9=A0=81=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/StockIssue/ExpiryHandleTab.tsx | 187 +++++++++++++----- src/i18n/en/stockIssue.json | 9 +- src/i18n/zh/stockIssue.json | 17 +- 3 files changed, 153 insertions(+), 60 deletions(-) diff --git a/src/components/StockIssue/ExpiryHandleTab.tsx b/src/components/StockIssue/ExpiryHandleTab.tsx index c661966d..fbb12ea2 100644 --- a/src/components/StockIssue/ExpiryHandleTab.tsx +++ b/src/components/StockIssue/ExpiryHandleTab.tsx @@ -24,6 +24,11 @@ import { DialogActions, DialogContent, DialogTitle, + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, Tab, Tabs, Tooltip, @@ -36,17 +41,21 @@ type SearchQuery = { itemCode: string; itemName: string; lotNo: string; - daysAhead: string; }; type SearchParamNames = keyof SearchQuery; type ResultBucket = "expired" | "today" | "upcoming"; const DEFAULT_DAYS_AHEAD = 7; -const MAX_DAYS_AHEAD = 365; +const MIN_DAYS_AHEAD = 1; +const MAX_DAYS_AHEAD = 14; +const DAYS_AHEAD_OPTIONS = Array.from( + { length: MAX_DAYS_AHEAD - MIN_DAYS_AHEAD + 1 }, + (_, i) => MIN_DAYS_AHEAD + i, +); function parseDaysAhead(raw: string | number | undefined): number { const n = typeof raw === "number" ? raw : Number.parseInt(String(raw ?? "").trim(), 10); - if (!Number.isFinite(n) || n < 0) return DEFAULT_DAYS_AHEAD; + if (!Number.isFinite(n) || n < MIN_DAYS_AHEAD) return DEFAULT_DAYS_AHEAD; return Math.min(Math.floor(n), MAX_DAYS_AHEAD); } @@ -105,7 +114,7 @@ function canHandleExpiryItem(item: ExpiryItemResult): boolean { return d != null && !d.isAfter(dayjs(), "day"); } -/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.3 | 2026-09-08 */ const ExpiryHandleTab: React.FC = () => { const BATCH_CHUNK_SIZE = 20; const { t } = useTranslation("stockIssue"); @@ -129,8 +138,10 @@ const ExpiryHandleTab: React.FC = () => { const expirySubmitInFlightRef = useRef>(new Set()); const batchSubmitInFlightRef = useRef(false); const exportInFlightRef = useRef(false); + const searchInFlightRef = useRef(false); const [exporting, setExporting] = useState<"filtered" | "all" | null>(null); const [paging, setPaging] = useState({ pageNum: 1, pageSize: 10 }); + const [daysAheadDraft, setDaysAheadDraft] = useState(String(DEFAULT_DAYS_AHEAD)); const daysAhead = lastFilters.daysAhead ?? DEFAULT_DAYS_AHEAD; @@ -162,14 +173,6 @@ const ExpiryHandleTab: React.FC = () => { { name: "itemCode", label: t("Item Code"), type: "text" }, { name: "itemName", label: t("Item"), type: "text" }, { name: "lotNo", label: t("Lot No."), type: "text" }, - { - name: "daysAhead", - label: t("Days ahead"), - type: "number", - defaultValue: String(DEFAULT_DAYS_AHEAD), - min: 0, - max: MAX_DAYS_AHEAD, - }, ], [t], ); @@ -294,13 +297,17 @@ const ExpiryHandleTab: React.FC = () => { const handleSearch = useCallback( async (query: Record) => { + if (searchInFlightRef.current) return; + const parsedDays = parseDaysAhead(daysAheadDraft); + setDaysAheadDraft(String(parsedDays)); setPaging((prev) => ({ ...prev, pageNum: 1 })); const filters: ExpiryItemFilter = { itemCode: query.itemCode?.trim() || undefined, itemName: query.itemName?.trim() || undefined, lotNo: query.lotNo?.trim() || undefined, - daysAhead: parseDaysAhead(query.daysAhead), + daysAhead: parsedDays, }; + searchInFlightRef.current = true; try { const result = await fetchExpiryItemList(filters); setLastFilters(filters); @@ -309,9 +316,48 @@ const ExpiryHandleTab: React.FC = () => { } catch (error) { console.error("Failed to search expiry items:", error); alert(t("Failed to load expiry items")); + } finally { + searchInFlightRef.current = false; } }, - [t], + [t, daysAheadDraft], + ); + + const applyDaysAhead = useCallback( + async (nextDays: number) => { + const parsedDays = parseDaysAhead(nextDays); + setDaysAheadDraft(String(parsedDays)); + if (parsedDays === daysAhead) return; + if (!hasSearched) { + setLastFilters((prev) => ({ ...prev, daysAhead: parsedDays })); + return; + } + if (searchInFlightRef.current) return; + searchInFlightRef.current = true; + try { + const filters: ExpiryItemFilter = { + ...lastFilters, + daysAhead: parsedDays, + }; + const result = await fetchExpiryItemList(filters); + setLastFilters(filters); + setExpiryItems(result); + setPaging((prev) => ({ ...prev, pageNum: 1 })); + } catch (error) { + console.error("Failed to search expiry items:", error); + alert(t("Failed to load expiry items")); + } finally { + searchInFlightRef.current = false; + } + }, + [daysAhead, hasSearched, lastFilters, t], + ); + + const handleDaysAheadChange = useCallback( + (event: SelectChangeEvent) => { + void applyDaysAhead(parseDaysAhead(event.target.value)); + }, + [applyDaysAhead], ); const handleExportExcel = useCallback( @@ -353,29 +399,37 @@ const ExpiryHandleTab: React.FC = () => { return ( - - - - - - - + + + + - + + + {resultTab === "upcoming" && ( + + {t("Days ahead")} + + + )} + + + + items={tabItems} diff --git a/src/i18n/en/stockIssue.json b/src/i18n/en/stockIssue.json index 7ee187b5..45590bce 100644 --- a/src/i18n/en/stockIssue.json +++ b/src/i18n/en/stockIssue.json @@ -6,7 +6,7 @@ "Bad Item Qty": "Bad Item Qty", "Bad Item Records": "Bad Item Records", "Batch Disposed All": "Batch Disposed All", - "Export Excel": "Export Excel", + "Export Excel": "Export this category", "Exporting...": "Exporting...", "Failed to export Excel": "Failed to export Excel", "Book Qty": "Book Qty", @@ -21,10 +21,11 @@ "Expiry on or before": "Expiry on or before", "Already expired": "Expiry not yet handle", "Expires today": "Expires today", - "Expires within 7 days": "Expires within 7 days", - "Expires within n days": "Expires within {{days}} days", + "Expires within 7 days": "Expires within X days", + "Expires within n days": "Expires within X days", + "Expires within X days": "Expires within X days", "All expiry items": "All", - "Export all in tab": "All", + "Export all in tab": "Export all", "Days ahead": "Days ahead", "Confirm batch dispose": "Confirm batch dispose", "Confirm batch dispose message": "Dispose {{count}} lot(s)? Remaining quantity will be fully stocked out.", diff --git a/src/i18n/zh/stockIssue.json b/src/i18n/zh/stockIssue.json index d90a2125..f9bec61e 100644 --- a/src/i18n/zh/stockIssue.json +++ b/src/i18n/zh/stockIssue.json @@ -6,7 +6,7 @@ "Bad Item Qty": "不良品數量", "Bad Item Records": "不良品處理紀錄", "Batch Disposed All": "批量處理完成", - "Export Excel": "匯出 Excel", + "Export Excel": "匯出目前分類", "Exporting...": "匯出中...", "Failed to export Excel": "匯出 Excel 失敗", "Book Qty": "帳面庫存", @@ -21,13 +21,14 @@ "Expiry on or before": "到期日(含當日及以前)", "Already expired": "過期尚未處理", "Expires today": "今日到期", - "Expires within 7 days": "未來 7 日到期", - "Expires within n days": "未來 {{days}} 日到期", + "Expires within 7 days": "未來 X 日到期", + "Expires within n days": "未來 X 日到期", + "Expires within X days": "未來 X 日到期", "All expiry items": "全部", - "Export all in tab": "全部", + "Export all in tab": "匯出全部", "Days ahead": "未來天數", "Confirm batch dispose": "確認批量處置", - "Confirm batch dispose message": "將處置 {{count}} 筆批號,剩餘數量會全部出倉。確定?", + "Confirm batch dispose message": "將處置 {{count}} 筆批號,數量會全部出倉。確定?", "Expiry End Date": "到期日(結束)", "Expiry Item": "過期", "Expiry Item Handle": "過期品處理", @@ -43,7 +44,7 @@ "Handled End Date": "處理日期(結束)", "Handler": "處理人", "Issue Qty": "問題數量", - "Item": "貨品", + "Item": "貨品名稱", "Item Code": "貨品編號", "Item not found": "找不到貨品", "Item selected": "已選擇貨品", @@ -66,7 +67,7 @@ "Processing...": "處理中...", "Quantity exceeds available quantity": "數量超過可用數量", "Remain available Quantity": "剩餘可用數量", - "Remaining Qty": "剩餘數量", + "Remaining Qty": "數量", "Remark": "備註", "Remarks": "備註", "Reset": "重置", @@ -85,7 +86,7 @@ "Submitting...": "提交中...", "Type": "類型", "Unknown error": "未知錯誤", - "UoM": "單位", + "UoM": "庫存單位", "User ID is required": "需要用戶ID", "Warehouse": "倉庫", "available": "可用",