|
- "use client";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
- import SearchBox, { Criterion } from "../SearchBox";
- import { useCallback, useMemo, useState } from "react";
- import { useTranslation } from "react-i18next";
- import SearchResults, { Column } from "../SearchResults/index";
- import { SessionWithTokens } from "@/config/authConfig";
- import {
- batchSubmitBadItem,
- batchSubmitExpiryItem,
- batchSubmitMissItem,
- ExpiryItemResult,
- fetchExpiryItemList,
- StockIssueLists,
- StockIssueResult,
- submitBadItem,
- submitExpiryItem,
- submitMissItem,
- } from "@/app/api/stockIssue/actions";
- import { Box, Button, Tab, Tabs } from "@mui/material";
- import { useSession } from "next-auth/react";
- import SubmitIssueForm from "./SubmitIssueForm";
-
- interface Props {
- dataList: StockIssueLists;
- }
-
- type SearchQuery = {
- lotNo: string;
- itemCode: string;
- itemName: string;
- expiryDate: string;
- };
- type SearchParamNames = keyof SearchQuery;
-
- const SearchPage: React.FC<Props> = ({ dataList }) => {
- const BATCH_CHUNK_SIZE = 20;
- const { t } = useTranslation("inventory");
- const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss");
- const [search, setSearch] = useState<SearchQuery>({
- lotNo: "",
- itemCode: "",
- itemName: "",
- expiryDate: "",
- });
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
- const [formOpen, setFormOpen] = useState(false);
- const [selectedLotId, setSelectedLotId] = useState<number | null>(null);
- const [selectedItemId, setSelectedItemId] = useState<number>(0);
- const [selectedIssueType, setSelectedIssueType] = useState<"miss" | "bad">("miss");
-
- const [missItems, setMissItems] = useState<StockIssueResult[]>(
- dataList.missItems,
- );
- const [badItems, setBadItems] = useState<StockIssueResult[]>(
- dataList.badItems,
- );
- const [expiryItems, setExpiryItems] = useState<ExpiryItemResult[]>(
- dataList.expiryItems,
- );
- const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
- const [submittingIds, setSubmittingIds] = useState<Set<number>>(new Set());
- const [batchSubmitting, setBatchSubmitting] = useState(false);
- const [batchProgress, setBatchProgress] = useState<{ done: number; total: number } | null>(null);
- const [paging, setPaging] = useState({ pageNum: 1, pageSize: 10 });
- const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
- () => {
- if (tab === "expiry") {
- return [
- {
- label: t("Item Code"),
- paramName: "itemCode",
- type: "text",
- },
- {
- label: t("Item"),
- paramName: "itemName",
- type: "text",
- },
- {
- label: t("Expiry Date"),
- paramName: "expiryDate",
- type: "date",
- },
- ];
- }
-
- return [
- {
- label: t("Lot No."),
- paramName: "lotNo",
- type: "text",
- },
- ];
- },
- [t, tab],
- );
-
- const filterBySearch = useCallback(
- <T extends { lotNo: string | null }>(items: T[]): T[] => {
- if (!search.lotNo) return items;
- const keyword = search.lotNo.toLowerCase();
- return items.filter(
- (i) => i.lotNo && i.lotNo.toLowerCase().includes(keyword),
- );
- },
- [search.lotNo],
- );
-
- const handleSubmitSingle = useCallback(
- async (id: number) => {
- if (!currentUserId) {
- alert(t("User ID is required"));
- return;
- }
-
- // Find the item to get lotId
- let lotId: number | null = null;
- let itemId = 0;
-
- if (tab === "miss") {
- const item = missItems.find((i) => i.id === id);
- if (item) {
- lotId = item.lotId;
- itemId = item.itemId;
- }
- } else if (tab === "bad") {
- const item = badItems.find((i) => i.id === id);
- if (item) {
- lotId = item.lotId;
- itemId = item.itemId;
- }
- } else if (tab === "expiry") {
- const item = expiryItems.find((i) => i.id === id);
- if (!item) {
- alert(t("Item not found"));
- return;
- }
-
- try {
- // 如果想要 loading 效果,可以这里把 id 加进 submittingIds
- await submitExpiryItem(item.id, currentUserId);
- // 成功后,从列表移除这一行,或直接 reload
- // setExpiryItems(prev => prev.filter(i => i.id !== id));
- window.location.reload();
- } catch (e) {
- console.error("submitExpiryItem failed:", e);
- const errMsg = e instanceof Error ? e.message : t("Unknown error");
- alert(`${t("Failed to submit expiry item")}: ${errMsg}`);
- }
- return; // 记得 return,避免再走到下面的 lotId/itemId 分支
- }
-
- if (lotId && itemId) {
- setSelectedLotId(lotId);
- setSelectedItemId(itemId);
- setSelectedIssueType(tab === "miss" ? "miss" : "bad");
- setFormOpen(true);
- } else {
- alert(t("Item not found"));
- }
- },
- [tab, currentUserId, t, missItems, badItems, expiryItems]
- );
-
- const handleFormSuccess = useCallback(() => {
- // Refresh the lists
- if (tab === "miss") {
- // Reload miss items - you may need to add a refresh function
- window.location.reload(); // Or use a proper refresh mechanism
- } else if (tab === "bad") {
- // Reload bad items
- window.location.reload(); // Or use a proper refresh mechanism
- }
- }, [tab]);
-
- const handleSubmitSelected = useCallback(async () => {
- if (!currentUserId) return;
-
- // Get all IDs from the current tab's filtered items
- let allIds: number[] = [];
- if (tab === "miss") {
- const items = filterBySearch(missItems);
- allIds = items.map((item) => item.id);
- } else if (tab === "bad") {
- const items = filterBySearch(badItems);
- allIds = items.map((item) => item.id);
- } else {
- const items = filterBySearch(expiryItems);
- allIds = items.map((item) => item.id);
- }
-
- if (allIds.length === 0) return;
-
- setBatchSubmitting(true);
- setBatchProgress({ done: 0, total: allIds.length });
- try {
- for (let i = 0; i < allIds.length; i += BATCH_CHUNK_SIZE) {
- const chunkIds = allIds.slice(i, i + BATCH_CHUNK_SIZE);
-
- if (tab === "miss") {
- await batchSubmitMissItem(chunkIds, currentUserId);
- setMissItems((prev) => prev.filter((item) => !chunkIds.includes(item.id)));
- } else if (tab === "bad") {
- await batchSubmitBadItem(chunkIds, currentUserId);
- setBadItems((prev) => prev.filter((item) => !chunkIds.includes(item.id)));
- } else {
- await batchSubmitExpiryItem(chunkIds, currentUserId);
- setExpiryItems((prev) => prev.filter((item) => !chunkIds.includes(item.id)));
- }
-
- setBatchProgress({
- done: Math.min(i + chunkIds.length, allIds.length),
- total: allIds.length,
- });
- }
-
- setSelectedIds([]);
- } catch (error) {
- console.error("Failed to submit selected items:", error);
- const partialDone = batchProgress?.done ?? 0;
- alert(
- `${t("Failed to submit")}: ${error instanceof Error ? error.message : "Unknown error"} (${partialDone}/${allIds.length})`
- );
- } finally {
- setBatchSubmitting(false);
- setBatchProgress(null);
- }
- }, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch, batchProgress, t]);
-
- const missColumns = useMemo<Column<StockIssueResult>[]>(
- () => [
- { name: "itemCode", label: t("Item Code") },
- { name: "itemDescription", label: t("Item") },
- { name: "lotNo", label: t("Lot No.") },
- { name: "storeLocation", label: t("Location") },
- {
- name: "bookQty",
- label: t("Book Qty"),
- renderCell: (item) => (
- <>{item.bookQty?.toFixed(2) ?? "0"} {item.uomDesc ?? ""}</>
- ),
- },
- { name: "issueQty", label: t("Miss Qty") },
- { name: "uomDesc", label: t("UoM"), renderCell: (item) => (
- <>{item.uomDesc ?? ""}</>
- ) },
- {
- 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("Processing...") : t("Looked")}
- </Button>
- ),
- },
- ],
- [t, handleSubmitSingle, submittingIds, currentUserId],
- );
-
- const badColumns = useMemo<Column<StockIssueResult>[]>(
- () => [
- { name: "itemCode", label: t("Item Code") },
- { name: "itemDescription", label: t("Item") },
- { name: "lotNo", label: t("Lot No.") },
- { name: "storeLocation", label: t("Location") },
- { name: "issueQty", label: t("Defective Qty") },
- { name: "uomDesc", label: t("UoM"), renderCell: (item) => (
- <>{item.uomDesc ?? ""}</>
- ) },
- {
- 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>
- ),
- },
- ],
- [t, handleSubmitSingle, submittingIds, currentUserId],
- );
-
- const expiryColumns = useMemo<Column<ExpiryItemResult>[]>(
- () => [
- { name: "itemCode", label: t("Item Code") },
- { name: "itemDescription", label: t("Item") },
- { name: "lotNo", label: t("Lot No.") },
- { name: "storeLocation", label: t("Location") },
- {
- 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;
- },
- },
- { name: "remainingQty", label: t("Remaining Qty") },
- {
- 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>
- ),
- },
- ],
- [t, handleSubmitSingle, submittingIds, currentUserId],
- );
-
- const handleSearch = useCallback(async (query: Record<SearchParamNames, string>) => {
- setSearch(query);
- setPaging((prev) => ({ ...prev, pageNum: 1 }));
-
- if (tab !== "expiry") {
- return;
- }
-
- try {
- const result = await fetchExpiryItemList({
- itemCode: query.itemCode?.trim() || undefined,
- itemName: query.itemName?.trim() || undefined,
- expiryDate: query.expiryDate || undefined,
- });
- setExpiryItems(result);
- setSelectedIds([]);
- } catch (error) {
- console.error("Failed to search expiry items:", error);
- alert(t("Failed to load expiry items"));
- }
- }, [tab, t]);
-
- const handleTabChange = useCallback(
- (_: React.SyntheticEvent, value: string) => {
- setTab(value as "miss" | "bad" | "expiry");
- setSelectedIds([]);
- setPaging((prev) => ({ ...prev, pageNum: 1 })); // 新增:切 Tab 时回到第 1 页
- },
- [],
- );
-
- const renderCurrentTab = () => {
- if (tab === "miss") {
- const items = filterBySearch(missItems);
- return (
- <SearchResults<StockIssueResult>
- items={items}
- columns={missColumns}
- pagingController={paging}
- checkboxIds={selectedIds}
- setPagingController={setPaging}
- setCheckboxIds={setSelectedIds}
- />
- );
- }
-
- if (tab === "bad") {
- const items = filterBySearch(badItems);
- return (
- <SearchResults<StockIssueResult>
- items={items}
- columns={badColumns}
- pagingController={paging}
- setPagingController={setPaging}
- checkboxIds={selectedIds}
- setCheckboxIds={setSelectedIds}
- />
- );
- }
-
- const items = filterBySearch(expiryItems);
- return (
- <SearchResults<ExpiryItemResult>
- items={items}
- columns={expiryColumns}
- pagingController={paging}
- setPagingController={setPaging}
- checkboxIds={selectedIds}
- setCheckboxIds={setSelectedIds}
- />
- );
- };
-
- return (
- <Box>
- <Tabs value={tab} onChange={handleTabChange} sx={{ mb: 2 }}>
- <Tab value="miss" label={t("Miss Item")} />
- <Tab value="bad" label={t("Bad Item")} />
- <Tab value="expiry" label={t("Expiry Item")} />
- </Tabs>
-
- <SearchBox<SearchParamNames>
- criteria={searchCriteria}
- onSearch={handleSearch}
- />
-
- {tab === "expiry" && (
- <Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}>
- <Button
- variant="contained"
- color="primary"
- onClick={handleSubmitSelected}
- disabled={batchSubmitting || !currentUserId}
- >
- {batchSubmitting
- ? `${t("Disposing...")} ${batchProgress ? `(${batchProgress.done}/${batchProgress.total})` : ""}`
- : t("Batch Disposed All")}
- </Button>
- </Box>
- )}
-
- {renderCurrentTab()}
- <SubmitIssueForm
- open={formOpen}
- onClose={() => setFormOpen(false)}
- lotId={selectedLotId}
- itemId={selectedItemId}
- issueType={selectedIssueType}
- currentUserId={currentUserId || 0}
- onSuccess={handleFormSuccess}
- />
- </Box>
- );
- };
-
- export default SearchPage;
|