Bläddra i källkod

過期品處理

fix負數倉
CANCERYS\kw093 1 vecka sedan
förälder
incheckning
c8c9ae0ee8
7 ändrade filer med 456 tillägg och 83 borttagningar
  1. +13
    -2
      src/app/api/stockIssue/actions.ts
  2. +67
    -0
      src/app/api/stockIssue/client.ts
  3. +275
    -59
      src/components/StockIssue/ExpiryHandleTab.tsx
  4. +45
    -18
      src/components/StockIssue/StockIssueRecordTab.tsx
  5. +22
    -2
      src/components/StockIssue/StockIssueSearchPanel.tsx
  6. +17
    -1
      src/i18n/en/stockIssue.json
  7. +17
    -1
      src/i18n/zh/stockIssue.json

+ 13
- 2
src/app/api/stockIssue/actions.ts Visa fil

@@ -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<ExpiryItemResult[]>(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);


+ 67
- 0
src/app/api/stockIssue/client.ts Visa fil

@@ -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<void> {
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);
}

+ 275
- 59
src/components/StockIssue/ExpiryHandleTab.tsx Visa fil

@@ -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<ExpiryItemResult[]>([]);
const [lastFilters, setLastFilters] = useState<ExpiryItemFilter>({
daysAhead: DEFAULT_DAYS_AHEAD,
});
const [hasSearched, setHasSearched] = useState(false);
const [resultTab, setResultTab] = useState<ResultBucket>("expired");
const [submittingIds, setSubmittingIds] = useState<Set<number>>(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<Set<number>>(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<SearchParamNames>[] = 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<Column<ExpiryItemResult>[]>(
() => [
@@ -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) => (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleSubmitSingle(item.id)}
disabled={submittingIds.has(item.id) || !currentUserId}
>
{submittingIds.has(item.id) ? t("Disposing...") : t("Disposed")}
</Button>
),
renderCell: (item) => {
const canHandle = canHandleExpiryItem(item);
const disposing = submittingIds.has(item.id);
const button = (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleSubmitSingle(item.id)}
disabled={disposing || !currentUserId || !canHandle}
>
{disposing ? t("Disposing...") : t("Disposed")}
</Button>
);
if (canHandle) return button;
return (
<Tooltip title={t("Not yet due; cannot dispose until the expiry date")}>
<span>{button}</span>
</Tooltip>
);
},
},
],
[t, handleSubmitSingle, submittingIds, currentUserId],
@@ -180,12 +295,16 @@ const ExpiryHandleTab: React.FC = () => {
const handleSearch = useCallback(
async (query: Record<SearchParamNames, string>) => {
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 (
<Box>
<StockIssueSearchPanel fields={searchFields} onSearch={handleSearch} />
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}>
<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}
>
{exporting === "filtered" ? t("Exporting...") : t("Export Excel")}
</Button>
<Button
variant="outlined"
startIcon={<FileDownload />}
onClick={() => handleExportExcel("all")}
disabled={!hasSearched || exporting != null}
>
{exporting === "all" ? t("Exporting...") : t("Export all in tab")}
</Button>
<Button
variant="contained"
color="primary"
onClick={handleSubmitAll}
disabled={batchSubmitting || !currentUserId || expiryItems.length === 0}
onClick={() => setBatchConfirmOpen(true)}
disabled={
batchSubmitting || !currentUserId || handleableIds.length === 0
}
>
{batchSubmitting
? `${t("Disposing...")} ${batchProgress ? `(${batchProgress.done}/${batchProgress.total})` : ""}`
@@ -216,12 +398,46 @@ const ExpiryHandleTab: React.FC = () => {
</Button>
</Box>
<SearchResults<ExpiryItemResult>
items={pagedItems}
items={tabItems}
columns={expiryColumns}
pagingController={paging}
setPagingController={setPaging}
totalCount={expiryItems.length}
totalCount={tabItems.length}
/>
<Dialog
open={batchConfirmOpen}
onClose={() => {
if (!batchSubmitting) setBatchConfirmOpen(false);
}}
fullWidth
maxWidth="xs"
>
<DialogTitle>{t("Confirm batch dispose")}</DialogTitle>
<DialogContent>
<Typography>
{t("Confirm batch dispose message", { count: handleableIds.length })}
</Typography>
</DialogContent>
<DialogActions>
<Button
onClick={() => setBatchConfirmOpen(false)}
disabled={batchSubmitting}
>
{t("Cancel")}
</Button>
<Button
variant="contained"
color="primary"
disabled={batchSubmitting || handleableIds.length === 0}
onClick={async () => {
setBatchConfirmOpen(false);
await handleSubmitAll();
}}
>
{tCommon("Confirm")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};


+ 45
- 18
src/components/StockIssue/StockIssueRecordTab.tsx Visa fil

@@ -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<Props> = ({ kind }) => {
const { t } = useTranslation("stockIssue");
const [items, setItems] = useState<StockIssueHandleRecord[]>([]);
@@ -40,27 +43,47 @@ const StockIssueRecordTab: React.FC<Props> = ({ kind }) => {
lotNo: "",
startDate: "",
endDate: "",
handledStartDate: "",
handledEndDate: "",
});
const hasSearchedRef = useRef(false);
const prevPagingRef = useRef(paging);

const searchFields: StockIssueSearchField<SearchParamNames>[] = 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<SearchParamNames>[] = [
{ 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<Props> = ({ 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<Props> = ({ kind }) => {
},
{
name: "uomDesc",
label: t("UOM"),
label: t("UoM"),
renderCell: (row) => (
<>
{row.uomDesc ?? ""}
@@ -179,8 +204,10 @@ const StockIssueRecordTab: React.FC<Props> = ({ 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]);



+ 22
- 2
src/components/StockIssue/StockIssueSearchPanel.tsx Visa fil

@@ -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<K extends string> {
name: K;
@@ -36,6 +36,9 @@ export interface StockIssueSearchField<K extends string> {
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<K extends string> {
@@ -46,6 +49,7 @@ interface Props<K extends string> {
disabled?: boolean;
}

/** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */
function StockIssueSearchPanel<K extends string>({
fields,
onSearch,
@@ -60,7 +64,8 @@ function StockIssueSearchPanel<K extends string>({
return fields.reduce(
(acc, field) => {
acc[field.name] =
field.type === "select" ? "All" : "";
field.defaultValue ??
(field.type === "select" ? "All" : "");
return acc;
},
{} as Record<K, string>,
@@ -127,6 +132,21 @@ function StockIssueSearchPanel<K extends string>({
disabled={disabled}
/>
)}
{field.type === "number" && (
<TextField
label={field.label}
type="number"
fullWidth
value={values[field.name] ?? ""}
onChange={handleTextChange(field.name)}
disabled={disabled}
inputProps={{
min: field.min,
max: field.max,
step: 1,
}}
/>
)}
{field.type === "select" && (
<FormControl fullWidth disabled={disabled}>
<InputLabel>{field.label}</InputLabel>


+ 17
- 1
src/i18n/en/stockIssue.json Visa fil

@@ -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",


+ 17
- 1
src/i18n/zh/stockIssue.json Visa fil

@@ -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": "貨品",


Laddar…
Avbryt
Spara