Преглед на файлове

期品兩個 Excel 按鈕/分頁文案

fix負數倉
CANCERYS\kw093 преди 1 седмица
родител
ревизия
79a4f85646
променени са 3 файла, в които са добавени 153 реда и са изтрити 60 реда
  1. +139
    -48
      src/components/StockIssue/ExpiryHandleTab.tsx
  2. +5
    -4
      src/i18n/en/stockIssue.json
  3. +9
    -8
      src/i18n/zh/stockIssue.json

+ 139
- 48
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<Set<number>>(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<SearchParamNames, string>) => {
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<string>) => {
void applyDaysAhead(parseDaysAhead(event.target.value));
},
[applyDaysAhead],
);

const handleExportExcel = useCallback(
@@ -353,29 +399,37 @@ const ExpiryHandleTab: React.FC = () => {
return (
<Box>
<StockIssueSearchPanel fields={searchFields} onSearch={handleSearch} />
<Tabs value={resultTab} onChange={handleResultTabChange} sx={{ mb: 2 }}>
<Tab
value="expired"
label={`${t("Already expired")} (${itemsByBucket.expired.length})`}
/>
<Tab
value="today"
label={`${t("Expires today")} (${itemsByBucket.today.length})`}
/>
<Tab
value="upcoming"
label={`${t("Expires within n days", { days: daysAhead })} (${itemsByBucket.upcoming.length})`}
/>
</Tabs>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1, mb: 1 }}>
<Button
variant="outlined"
startIcon={<FileDownload />}
onClick={() => handleExportExcel("filtered")}
disabled={!hasSearched || exporting != null || tabItems.length === 0}
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
gap: 1.5,
mb: 2,
flexWrap: "wrap",
}}
>
<Tabs
value={resultTab}
onChange={handleResultTabChange}
sx={{
minHeight: 48,
"& .MuiTab-root": { minHeight: 48, minWidth: 0, px: 2 },
}}
>
{exporting === "filtered" ? t("Exporting...") : t("Export Excel")}
</Button>
<Tab
value="expired"
label={`${t("Already expired")} (${itemsByBucket.expired.length})`}
/>
<Tab
value="today"
label={`${t("Expires today")} (${itemsByBucket.today.length})`}
/>
<Tab
value="upcoming"
label={`${t("Expires within X days")} (${itemsByBucket.upcoming.length})`}
/>
</Tabs>
<Button
variant="outlined"
startIcon={<FileDownload />}
@@ -384,18 +438,55 @@ const ExpiryHandleTab: React.FC = () => {
>
{exporting === "all" ? t("Exporting...") : t("Export all in tab")}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setBatchConfirmOpen(true)}
disabled={
batchSubmitting || !currentUserId || handleableIds.length === 0
}
>
{batchSubmitting
? `${t("Disposing...")} ${batchProgress ? `(${batchProgress.done}/${batchProgress.total})` : ""}`
: t("Batch Disposed All")}
</Button>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
mb: 1,
flexWrap: "wrap",
}}
>
{resultTab === "upcoming" && (
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel id="expiry-days-ahead-label">{t("Days ahead")}</InputLabel>
<Select
labelId="expiry-days-ahead-label"
label={t("Days ahead")}
value={String(parseDaysAhead(daysAheadDraft))}
onChange={handleDaysAheadChange}
>
{DAYS_AHEAD_OPTIONS.map((days) => (
<MenuItem key={days} value={String(days)}>
{days}
</MenuItem>
))}
</Select>
</FormControl>
)}
<Box sx={{ display: "flex", gap: 1, ml: "auto" }}>
<Button
variant="outlined"
startIcon={<FileDownload />}
onClick={() => handleExportExcel("filtered")}
disabled={!hasSearched || exporting != null || tabItems.length === 0}
>
{exporting === "filtered" ? t("Exporting...") : t("Export Excel")}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setBatchConfirmOpen(true)}
disabled={
batchSubmitting || !currentUserId || handleableIds.length === 0
}
>
{batchSubmitting
? `${t("Disposing...")} ${batchProgress ? `(${batchProgress.done}/${batchProgress.total})` : ""}`
: t("Batch Disposed All")}
</Button>
</Box>
</Box>
<SearchResults<ExpiryItemResult>
items={tabItems}


+ 5
- 4
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.",


+ 9
- 8
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": "可用",


Зареждане…
Отказ
Запис