From f438b840dc6c078a0b9906bff42b341f3f04e857 Mon Sep 17 00:00:00 2001 From: Harry Groves Date: Mon, 7 Sep 2026 16:42:14 +0800 Subject: [PATCH] Linked excel download to stockIssue frontend. Added "download all" and custom "days remaining" features in the frontend. --- src/app/api/stockIssue/actions.ts | 4 + src/app/api/stockIssue/client.ts | 5 +- src/components/StockIssue/ExpiryHandleTab.tsx | 110 +++++++++++++----- .../StockIssue/StockIssueRecordTab.tsx | 2 +- .../StockIssue/StockIssueSearchPanel.tsx | 24 +++- src/i18n/en/stockIssue.json | 4 + src/i18n/zh/stockIssue.json | 4 + 7 files changed, 118 insertions(+), 35 deletions(-) diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index ec95c153..fa957cca 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -26,6 +26,8 @@ export interface ExpiryItemFilter { itemCode?: string; itemName?: string; lotNo?: string; + /** Inclusive lookahead from today; default 7. */ + daysAhead?: number; } export interface HandleBadItemRequest { @@ -66,11 +68,13 @@ export interface SearchStockIssueRecordParams { pageSize?: number; } +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.1 | 2026-09-07 */ export const fetchExpiryItemList = cache(async (filters?: ExpiryItemFilter) => { const params = new URLSearchParams(); if (filters?.itemCode) params.set("itemCode", filters.itemCode); if (filters?.itemName) params.set("itemName", filters.itemName); if (filters?.lotNo) params.set("lotNo", filters.lotNo); + if (filters?.daysAhead != null) params.set("daysAhead", String(filters.daysAhead)); const queryString = params.toString(); const url = `${BASE_API_URL}/pickExecution/issues/expiryItem${queryString ? `?${queryString}` : ""}`; return serverFetchJson(url, { diff --git a/src/app/api/stockIssue/client.ts b/src/app/api/stockIssue/client.ts index 357b02da..d1c8d754 100644 --- a/src/app/api/stockIssue/client.ts +++ b/src/app/api/stockIssue/client.ts @@ -12,9 +12,9 @@ export interface ExportExpiryItemExcelParams extends ExpiryItemFilter { } /** - * Partner backend contract (not implemented here): + * FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.1 | 2026-09-07 * GET /pickExecution/issues/expiryItem/excel - * Query: itemCode, itemName, lotNo, bucket (expired|today|upcoming) + * Query: itemCode, itemName, lotNo, daysAhead, bucket (expired|today|upcoming) * Response: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet * Optional Content-Disposition filename. */ @@ -25,6 +25,7 @@ export async function exportExpiryItemExcel( if (filters.itemCode) params.set("itemCode", filters.itemCode); if (filters.itemName) params.set("itemName", filters.itemName); if (filters.lotNo) params.set("lotNo", filters.lotNo); + if (filters.daysAhead != null) params.set("daysAhead", String(filters.daysAhead)); if (filters.bucket) params.set("bucket", filters.bucket); const queryString = params.toString(); diff --git a/src/components/StockIssue/ExpiryHandleTab.tsx b/src/components/StockIssue/ExpiryHandleTab.tsx index 6c1673a4..22ec36f5 100644 --- a/src/components/StockIssue/ExpiryHandleTab.tsx +++ b/src/components/StockIssue/ExpiryHandleTab.tsx @@ -36,10 +36,20 @@ 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; + +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; + return Math.min(Math.floor(n), MAX_DAYS_AHEAD); +} + function parseExpiryDayjs(rawValue: unknown): dayjs.Dayjs | null { const raw = String(rawValue ?? "").trim(); if (!raw) return null; @@ -74,13 +84,18 @@ function parseExpiryDayjs(rawValue: unknown): dayjs.Dayjs | null { return d.isValid() ? d : null; } -function getExpiryBucket(item: ExpiryItemResult): ResultBucket | null { +function getExpiryBucket( + item: ExpiryItemResult, + daysAhead: number, +): ResultBucket | null { const d = parseExpiryDayjs(item.expiryDate); if (!d) return null; const today = dayjs().startOf("day"); if (d.isBefore(today, "day")) return "expired"; if (d.isSame(today, "day")) return "today"; - if (!d.isAfter(today.add(7, "day"), "day")) return "upcoming"; + if (daysAhead > 0 && !d.isAfter(today.add(daysAhead, "day"), "day")) { + return "upcoming"; + } return null; } @@ -90,7 +105,7 @@ function canHandleExpiryItem(item: ExpiryItemResult): boolean { return d != null && !d.isAfter(dayjs(), "day"); } -/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.0 | 2026-09-07 */ +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.1 | 2026-09-07 */ const ExpiryHandleTab: React.FC = () => { const BATCH_CHUNK_SIZE = 20; const { t } = useTranslation("stockIssue"); @@ -99,7 +114,9 @@ const ExpiryHandleTab: React.FC = () => { const currentUserId = session?.id ? parseInt(session.id) : undefined; const [expiryItems, setExpiryItems] = useState([]); - const [lastFilters, setLastFilters] = useState({}); + const [lastFilters, setLastFilters] = useState({ + daysAhead: DEFAULT_DAYS_AHEAD, + }); const [hasSearched, setHasSearched] = useState(false); const [resultTab, setResultTab] = useState("expired"); const [submittingIds, setSubmittingIds] = useState>(new Set()); @@ -112,21 +129,27 @@ const ExpiryHandleTab: React.FC = () => { const expirySubmitInFlightRef = useRef>(new Set()); const batchSubmitInFlightRef = useRef(false); const exportInFlightRef = useRef(false); - const [exporting, setExporting] = useState(false); + const [exporting, setExporting] = useState<"filtered" | "all" | null>(null); const [paging, setPaging] = useState({ pageNum: 1, pageSize: 10 }); + const daysAhead = lastFilters.daysAhead ?? DEFAULT_DAYS_AHEAD; + const itemsByBucket = useMemo(() => { const expired: ExpiryItemResult[] = []; const today: ExpiryItemResult[] = []; const upcoming: ExpiryItemResult[] = []; for (const item of expiryItems) { - const bucket = getExpiryBucket(item); + const bucket = getExpiryBucket(item, daysAhead); if (bucket === "expired") expired.push(item); else if (bucket === "today") today.push(item); else if (bucket === "upcoming") upcoming.push(item); } - return { expired, today, upcoming }; - }, [expiryItems]); + return { + expired, + today, + upcoming, + }; + }, [expiryItems, daysAhead]); const tabItems = itemsByBucket[resultTab]; const handleableIds = useMemo( @@ -139,6 +162,14 @@ 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], ); @@ -268,6 +299,7 @@ const ExpiryHandleTab: React.FC = () => { itemCode: query.itemCode?.trim() || undefined, itemName: query.itemName?.trim() || undefined, lotNo: query.lotNo?.trim() || undefined, + daysAhead: parseDaysAhead(query.daysAhead), }; try { const result = await fetchExpiryItemList(filters); @@ -282,24 +314,34 @@ const ExpiryHandleTab: React.FC = () => { [t], ); - const handleExportExcel = useCallback(async () => { - if (!hasSearched) return; - if (exportInFlightRef.current) return; - exportInFlightRef.current = true; - setExporting(true); - try { - await exportExpiryItemExcel({ - ...lastFilters, - bucket: resultTab, - }); - } catch (error) { - console.error("Failed to export expiry items:", error); - alert(t("Failed to export Excel")); - } finally { - setExporting(false); - exportInFlightRef.current = false; - } - }, [hasSearched, lastFilters, resultTab, t]); + const handleExportExcel = useCallback( + async (mode: "filtered" | "all") => { + if (mode === "filtered" && !hasSearched) return; + if (exportInFlightRef.current) return; + exportInFlightRef.current = true; + setExporting(mode); + try { + await exportExpiryItemExcel( + mode === "all" + ? { + daysAhead, + bucket: resultTab, + } + : { + ...lastFilters, + bucket: resultTab, + }, + ); + } catch (error) { + console.error("Failed to export expiry items:", error); + alert(t("Failed to export Excel")); + } finally { + setExporting(null); + exportInFlightRef.current = false; + } + }, + [hasSearched, lastFilters, resultTab, daysAhead, t], + ); const handleResultTabChange = useCallback( (_: React.SyntheticEvent, value: string) => { @@ -323,17 +365,25 @@ const ExpiryHandleTab: React.FC = () => { /> +