From c8c9ae0ee85a2802b0d2ddadd10175341cfa22d8 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 8 Sep 2026 17:37:08 +0800 Subject: [PATCH] =?UTF-8?q?=20=E9=81=8E=E6=9C=9F=E5=93=81=E8=99=95?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/stockIssue/actions.ts | 15 +- src/app/api/stockIssue/client.ts | 67 ++++ src/components/StockIssue/ExpiryHandleTab.tsx | 334 ++++++++++++++---- .../StockIssue/StockIssueRecordTab.tsx | 63 +++- .../StockIssue/StockIssueSearchPanel.tsx | 24 +- src/i18n/en/stockIssue.json | 18 +- src/i18n/zh/stockIssue.json | 18 +- 7 files changed, 456 insertions(+), 83 deletions(-) create mode 100644 src/app/api/stockIssue/client.ts diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 5461e1ec..5b30c36c 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -17,12 +17,17 @@ export interface ExpiryItemResult { storeLocation: string | null; expiryDate: string | null; remainingQty: number; + uomDesc?: string | null; + /** True when expiryDate is today or earlier. */ + canHandle?: boolean; } export interface ExpiryItemFilter { - expiryDate?: string; itemCode?: string; itemName?: string; + lotNo?: string; + /** Inclusive lookahead from today; default 7. */ + daysAhead?: number; } export interface HandleBadItemRequest { @@ -54,6 +59,8 @@ export interface StockIssueHandleRecord { export interface SearchStockIssueRecordParams { startDate?: string; endDate?: string; + handledStartDate?: string; + handledEndDate?: string; itemCode?: string; itemName?: string; lotNo?: string; @@ -61,11 +68,13 @@ export interface SearchStockIssueRecordParams { pageSize?: number; } +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */ export const fetchExpiryItemList = cache(async (filters?: ExpiryItemFilter) => { const params = new URLSearchParams(); - if (filters?.expiryDate) params.set("expiryDate", filters.expiryDate); 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, { @@ -107,6 +116,8 @@ export async function fetchExpiryItemRecords(params: SearchStockIssueRecordParam const qs = new URLSearchParams(); if (params.startDate) qs.set("startDate", params.startDate); if (params.endDate) qs.set("endDate", params.endDate); + if (params.handledStartDate) qs.set("handledStartDate", params.handledStartDate); + if (params.handledEndDate) qs.set("handledEndDate", params.handledEndDate); if (params.itemCode) qs.set("itemCode", params.itemCode); if (params.itemName) qs.set("itemName", params.itemName); if (params.lotNo) qs.set("lotNo", params.lotNo); diff --git a/src/app/api/stockIssue/client.ts b/src/app/api/stockIssue/client.ts new file mode 100644 index 00000000..264d5963 --- /dev/null +++ b/src/app/api/stockIssue/client.ts @@ -0,0 +1,67 @@ +"use client"; + +import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; +import type { ExpiryItemFilter } from "@/app/api/stockIssue/actions"; + +/** Result tab currently shown; backend may filter the workbook by this bucket. */ +export type ExpiryExportBucket = "expired" | "today" | "upcoming"; + +export interface ExportExpiryItemExcelParams extends ExpiryItemFilter { + bucket?: ExpiryExportBucket; +} + +/** + * FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 + * GET /pickExecution/issues/expiryItem/excel + * Query: itemCode, itemName, lotNo, daysAhead, bucket (expired|today|upcoming; omit for all categories) + * Response: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + * Optional Content-Disposition filename. + */ +export async function exportExpiryItemExcel( + filters: ExportExpiryItemExcelParams, +): Promise { + 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)); + if (filters.bucket) params.set("bucket", filters.bucket); + + const queryString = params.toString(); + const url = `${NEXT_PUBLIC_API_URL}/pickExecution/issues/expiryItem/excel${queryString ? `?${queryString}` : ""}`; + + const response = await clientAuthFetch(url, { + method: "GET", + headers: { + Accept: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + }); + + if (response.status === 401 || response.status === 403) { + throw new Error("Unauthorized"); + } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = downloadUrl; + + const contentDisposition = response.headers.get("Content-Disposition"); + let fileName = "expiry-items.xlsx"; + if (contentDisposition?.includes("filename=")) { + fileName = contentDisposition + .split("filename=")[1] + .split(";")[0] + .replace(/"/g, ""); + } + + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(downloadUrl); +} diff --git a/src/components/StockIssue/ExpiryHandleTab.tsx b/src/components/StockIssue/ExpiryHandleTab.tsx index a582c4e0..c661966d 100644 --- a/src/components/StockIssue/ExpiryHandleTab.tsx +++ b/src/components/StockIssue/ExpiryHandleTab.tsx @@ -11,42 +11,165 @@ import SearchResults, { Column } from "@/components/SearchResults/index"; import { SessionWithTokens } from "@/config/authConfig"; import { batchSubmitExpiryItem, + ExpiryItemFilter, ExpiryItemResult, fetchExpiryItemList, submitExpiryItem, } from "@/app/api/stockIssue/actions"; -import { Box, Button } from "@mui/material"; +import { exportExpiryItemExcel } from "@/app/api/stockIssue/client"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Tab, + Tabs, + Tooltip, + Typography, +} from "@mui/material"; +import FileDownload from "@mui/icons-material/FileDownload"; import { useSession } from "next-auth/react"; type SearchQuery = { itemCode: string; itemName: string; - expiryDate: 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; + let d: dayjs.Dayjs; + if (raw.includes(",")) { + const parts = raw.split(",").map((s) => parseInt(s.trim(), 10)); + const [y, m, d_] = parts; + if ( + parts.length >= 3 && + y != null && + m != null && + d_ != null && + !Number.isNaN(y) && + !Number.isNaN(m) && + !Number.isNaN(d_) + ) { + d = dayjs(new Date(y, m - 1, d_)); + } else { + d = dayjs(""); + } + } else if (/^\d{4}-\d{2}-\d{2}/.test(raw)) { + d = dayjs(raw.slice(0, 10)); + } else { + let normalized = raw; + if (raw.length === 7) { + normalized = raw.slice(0, 4) + "0" + raw.slice(4, 5) + raw.slice(5, 7); + } else if (raw.length === 6) { + normalized = raw.slice(0, 4) + "0" + raw.slice(4, 5) + "0" + raw.slice(5, 6); + } + d = dayjs(normalized, "YYYYMMDD", true); + } + return d.isValid() ? d : 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 (daysAhead > 0 && !d.isAfter(today.add(daysAhead, "day"), "day")) { + return "upcoming"; + } + return null; +} +function canHandleExpiryItem(item: ExpiryItemResult): boolean { + if (typeof item.canHandle === "boolean") return item.canHandle; + const d = parseExpiryDayjs(item.expiryDate); + return d != null && !d.isAfter(dayjs(), "day"); +} + +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */ const ExpiryHandleTab: React.FC = () => { const BATCH_CHUNK_SIZE = 20; const { t } = useTranslation("stockIssue"); + const { t: tCommon } = useTranslation("common"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const [expiryItems, setExpiryItems] = 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()); const [batchSubmitting, setBatchSubmitting] = useState(false); + const [batchConfirmOpen, setBatchConfirmOpen] = useState(false); const [batchProgress, setBatchProgress] = useState<{ done: number; total: number; } | null>(null); const expirySubmitInFlightRef = useRef>(new Set()); const batchSubmitInFlightRef = useRef(false); + const exportInFlightRef = useRef(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, 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, daysAhead]); + + const tabItems = itemsByBucket[resultTab]; + const handleableIds = useMemo( + () => tabItems.filter(canHandleExpiryItem).map((item) => item.id), + [tabItems], + ); + const searchFields: StockIssueSearchField[] = useMemo( () => [ { name: "itemCode", label: t("Item Code"), type: "text" }, { name: "itemName", label: t("Item"), type: "text" }, - { name: "expiryDate", label: t("Expiry Date"), type: "date" }, + { 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], ); @@ -62,6 +185,10 @@ const ExpiryHandleTab: React.FC = () => { alert(t("Item not found")); return; } + if (!canHandleExpiryItem(item)) { + alert(t("Not yet due; cannot dispose until the expiry date")); + return; + } if (expirySubmitInFlightRef.current.has(id)) return; try { @@ -88,7 +215,7 @@ const ExpiryHandleTab: React.FC = () => { const handleSubmitAll = useCallback(async () => { if (!currentUserId) return; if (batchSubmitInFlightRef.current) return; - const allIds = expiryItems.map((item) => item.id); + const allIds = tabItems.filter(canHandleExpiryItem).map((item) => item.id); if (allIds.length === 0) return; batchSubmitInFlightRef.current = true; @@ -114,7 +241,7 @@ const ExpiryHandleTab: React.FC = () => { setBatchProgress(null); batchSubmitInFlightRef.current = false; } - }, [currentUserId, expiryItems, t]); + }, [currentUserId, tabItems, t]); const expiryColumns = useMemo[]>( () => [ @@ -126,52 +253,40 @@ const ExpiryHandleTab: React.FC = () => { name: "expiryDate", label: t("Expiry Date"), renderCell: (item) => { - const raw = String(item.expiryDate ?? "").trim(); - if (!raw) return "—"; - let d; - if (raw.includes(",")) { - const parts = raw.split(",").map((s) => parseInt(s.trim(), 10)); - const [y, m, d_] = parts; - if ( - parts.length >= 3 && - y != null && - m != null && - d_ != null && - !Number.isNaN(y) && - !Number.isNaN(m) && - !Number.isNaN(d_) - ) { - d = dayjs(new Date(y, m - 1, d_)); - } else { - d = dayjs(""); - } - } else { - let normalized = raw; - if (raw.length === 7) { - normalized = raw.slice(0, 4) + "0" + raw.slice(4, 5) + raw.slice(5, 7); - } else if (raw.length === 6) { - normalized = raw.slice(0, 4) + "0" + raw.slice(4, 5) + "0" + raw.slice(5, 6); - } - d = dayjs(normalized, "YYYYMMDD", true); - } - return d.isValid() ? d.format(OUTPUT_DATE_FORMAT) : raw; + const d = parseExpiryDayjs(item.expiryDate); + return d ? d.format(OUTPUT_DATE_FORMAT) : String(item.expiryDate ?? "").trim() || "—"; }, }, { name: "remainingQty", label: t("Remaining Qty") }, + { + name: "uomDesc", + label: t("UoM"), + renderCell: (item) => item.uomDesc?.trim() || "—", + }, { name: "id", label: t("Action"), - renderCell: (item) => ( - - ), + renderCell: (item) => { + const canHandle = canHandleExpiryItem(item); + const disposing = submittingIds.has(item.id); + const button = ( + + ); + if (canHandle) return button; + return ( + + {button} + + ); + }, }, ], [t, handleSubmitSingle, submittingIds, currentUserId], @@ -180,12 +295,16 @@ const ExpiryHandleTab: React.FC = () => { const handleSearch = useCallback( async (query: Record) => { 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), + }; try { - const result = await fetchExpiryItemList({ - itemCode: query.itemCode?.trim() || undefined, - itemName: query.itemName?.trim() || undefined, - expiryDate: query.expiryDate || undefined, - }); + const result = await fetchExpiryItemList(filters); + setLastFilters(filters); + setHasSearched(true); setExpiryItems(result); } catch (error) { console.error("Failed to search expiry items:", error); @@ -195,20 +314,83 @@ const ExpiryHandleTab: React.FC = () => { [t], ); - const pagedItems = useMemo(() => { - const start = (paging.pageNum - 1) * paging.pageSize; - return expiryItems.slice(start, start + paging.pageSize); - }, [expiryItems, paging]); + const handleExportExcel = useCallback( + async (mode: "filtered" | "all") => { + if (!hasSearched) return; + if (exportInFlightRef.current) return; + exportInFlightRef.current = true; + setExporting(mode); + try { + await exportExpiryItemExcel( + mode === "all" + ? { + daysAhead, + } + : { + ...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) => { + setResultTab(value as ResultBucket); + setPaging((prev) => ({ ...prev, pageNum: 1 })); + }, + [], + ); return ( - + + + + + + + + - items={pagedItems} + items={tabItems} columns={expiryColumns} pagingController={paging} setPagingController={setPaging} - totalCount={expiryItems.length} + totalCount={tabItems.length} /> + { + if (!batchSubmitting) setBatchConfirmOpen(false); + }} + fullWidth + maxWidth="xs" + > + {t("Confirm batch dispose")} + + + {t("Confirm batch dispose message", { count: handleableIds.length })} + + + + + + + ); }; diff --git a/src/components/StockIssue/StockIssueRecordTab.tsx b/src/components/StockIssue/StockIssueRecordTab.tsx index 145441a4..461b2a39 100644 --- a/src/components/StockIssue/StockIssueRecordTab.tsx +++ b/src/components/StockIssue/StockIssueRecordTab.tsx @@ -22,6 +22,8 @@ type SearchQuery = { lotNo: string; startDate: string; endDate: string; + handledStartDate: string; + handledEndDate: string; }; type SearchParamNames = keyof SearchQuery; @@ -29,6 +31,7 @@ interface Props { kind: RecordKind; } +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */ const StockIssueRecordTab: React.FC = ({ kind }) => { const { t } = useTranslation("stockIssue"); const [items, setItems] = useState([]); @@ -40,27 +43,47 @@ const StockIssueRecordTab: React.FC = ({ kind }) => { lotNo: "", startDate: "", endDate: "", + handledStartDate: "", + handledEndDate: "", }); const hasSearchedRef = useRef(false); const prevPagingRef = useRef(paging); const searchFields: StockIssueSearchField[] = useMemo( - () => [ - { name: "itemCode", label: t("Item Code"), type: "text" }, - { name: "itemName", label: t("Item"), type: "text" }, - { name: "lotNo", label: t("Lot No."), type: "text" }, - { - name: "startDate", - label: kind === "expiry" ? t("Expiry Start Date") : t("Start Date"), - type: "date", - mirrorTo: "endDate", - }, - { - name: "endDate", - label: kind === "expiry" ? t("Expiry End Date") : t("End Date"), - type: "date", - }, - ], + () => { + const fields: StockIssueSearchField[] = [ + { name: "itemCode", label: t("Item Code"), type: "text" }, + { name: "itemName", label: t("Item"), type: "text" }, + { name: "lotNo", label: t("Lot No."), type: "text" }, + { + name: "startDate", + label: kind === "expiry" ? t("Expiry Start Date") : t("Start Date"), + type: "date", + mirrorTo: "endDate", + }, + { + name: "endDate", + label: kind === "expiry" ? t("Expiry End Date") : t("End Date"), + type: "date", + }, + ]; + if (kind === "expiry") { + fields.push( + { + name: "handledStartDate", + label: t("Handled Start Date"), + type: "date", + mirrorTo: "handledEndDate", + }, + { + name: "handledEndDate", + label: t("Handled End Date"), + type: "date", + }, + ); + } + return fields; + }, [t, kind], ); @@ -73,6 +96,8 @@ const StockIssueRecordTab: React.FC = ({ kind }) => { lotNo: query.lotNo?.trim() || undefined, startDate: query.startDate || undefined, endDate: query.endDate || undefined, + handledStartDate: query.handledStartDate || undefined, + handledEndDate: query.handledEndDate || undefined, pageNum: page.pageNum - 1, pageSize: page.pageSize, }; @@ -166,7 +191,7 @@ const StockIssueRecordTab: React.FC = ({ kind }) => { }, { name: "uomDesc", - label: t("UOM"), + label: t("UoM"), renderCell: (row) => ( <> {row.uomDesc ?? ""} @@ -179,8 +204,10 @@ const StockIssueRecordTab: React.FC = ({ kind }) => { renderCell: (row) => row.handlerName ?? (row.handlerId != null ? String(row.handlerId) : "—"), }, - { name: "remarks", label: t("Remarks") }, ); + if (kind !== "expiry") { + base.push({ name: "remarks", label: t("Remarks") }); + } return base; }, [t, kind]); diff --git a/src/components/StockIssue/StockIssueSearchPanel.tsx b/src/components/StockIssue/StockIssueSearchPanel.tsx index c0a29983..7c888291 100644 --- a/src/components/StockIssue/StockIssueSearchPanel.tsx +++ b/src/components/StockIssue/StockIssueSearchPanel.tsx @@ -25,7 +25,7 @@ import "dayjs/locale/zh-hk"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -export type StockIssueSearchFieldType = "text" | "select" | "date"; +export type StockIssueSearchFieldType = "text" | "select" | "date" | "number"; export interface StockIssueSearchField { name: K; @@ -36,6 +36,9 @@ export interface StockIssueSearchField { getOptionLabel?: (value: string) => string; /** When this date is picked, copy the same value to `mirrorTo`. */ mirrorTo?: K; + defaultValue?: string; + min?: number; + max?: number; } interface Props { @@ -46,6 +49,7 @@ interface Props { disabled?: boolean; } +/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */ function StockIssueSearchPanel({ fields, onSearch, @@ -60,7 +64,8 @@ function StockIssueSearchPanel({ return fields.reduce( (acc, field) => { acc[field.name] = - field.type === "select" ? "All" : ""; + field.defaultValue ?? + (field.type === "select" ? "All" : ""); return acc; }, {} as Record, @@ -127,6 +132,21 @@ function StockIssueSearchPanel({ disabled={disabled} /> )} + {field.type === "number" && ( + + )} {field.type === "select" && ( {field.label} diff --git a/src/i18n/en/stockIssue.json b/src/i18n/en/stockIssue.json index 5e6583f8..7ee187b5 100644 --- a/src/i18n/en/stockIssue.json +++ b/src/i18n/en/stockIssue.json @@ -6,15 +6,28 @@ "Bad Item Qty": "Bad Item Qty", "Bad Item Records": "Bad Item Records", "Batch Disposed All": "Batch Disposed All", + "Export Excel": "Export Excel", + "Exporting...": "Exporting...", + "Failed to export Excel": "Failed to export Excel", "Book Qty": "Book Qty", "Cancel": "Cancel", "Code": "Code", "Defective Qty": "Defective Qty", - "Disposed": "Disposed", + "Disposed": "Expiry handle", "Disposing...": "Disposing...", "DO Order Code": "DO Order Code", "End Date": "End Date", "Expiry Date": "Expiry Date", + "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", + "All expiry items": "All", + "Export all in tab": "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.", "Expiry End Date": "Expiry End Date", "Expiry Item": "Expiry Item", "Expiry Item Handle": "Expiry Item Handle", @@ -24,7 +37,10 @@ "Failed to load expiry items": "Failed to load expiry items", "Failed to submit": "Failed to submit", "Failed to submit expiry item": "Failed to submit expiry item", + "Not yet due; cannot dispose until the expiry date": "Not yet due; cannot dispose until the expiry date", "Handled Date": "Handled Date", + "Handled Start Date": "Handled Start Date", + "Handled End Date": "Handled End Date", "Handler": "Handler", "Issue Qty": "Issue Qty", "Item": "Item", diff --git a/src/i18n/zh/stockIssue.json b/src/i18n/zh/stockIssue.json index ca778989..d90a2125 100644 --- a/src/i18n/zh/stockIssue.json +++ b/src/i18n/zh/stockIssue.json @@ -6,15 +6,28 @@ "Bad Item Qty": "不良品數量", "Bad Item Records": "不良品處理紀錄", "Batch Disposed All": "批量處理完成", + "Export Excel": "匯出 Excel", + "Exporting...": "匯出中...", + "Failed to export Excel": "匯出 Excel 失敗", "Book Qty": "帳面庫存", "Cancel": "取消", "Code": "編號", "Defective Qty": "不良數量", - "Disposed": "已處置", + "Disposed": "過期處理", "Disposing...": "處理中...", "DO Order Code": "送貨單編號", "End Date": "結束日期", "Expiry Date": "到期日", + "Expiry on or before": "到期日(含當日及以前)", + "Already expired": "過期尚未處理", + "Expires today": "今日到期", + "Expires within 7 days": "未來 7 日到期", + "Expires within n days": "未來 {{days}} 日到期", + "All expiry items": "全部", + "Export all in tab": "全部", + "Days ahead": "未來天數", + "Confirm batch dispose": "確認批量處置", + "Confirm batch dispose message": "將處置 {{count}} 筆批號,剩餘數量會全部出倉。確定?", "Expiry End Date": "到期日(結束)", "Expiry Item": "過期", "Expiry Item Handle": "過期品處理", @@ -24,7 +37,10 @@ "Failed to load expiry items": "載入過期品失敗", "Failed to submit": "提交失敗", "Failed to submit expiry item": "提交過期品失敗", + "Not yet due; cannot dispose until the expiry date": "尚未到期,到期日當日才可處置", "Handled Date": "處理日期", + "Handled Start Date": "處理日期(開始)", + "Handled End Date": "處理日期(結束)", "Handler": "處理人", "Issue Qty": "問題數量", "Item": "貨品",