"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 = ({ dataList }) => { const BATCH_CHUNK_SIZE = 20; const { t } = useTranslation("inventory"); const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss"); const [search, setSearch] = useState({ 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(null); const [selectedItemId, setSelectedItemId] = useState(0); const [selectedIssueType, setSelectedIssueType] = useState<"miss" | "bad">("miss"); const [missItems, setMissItems] = useState( dataList.missItems, ); const [badItems, setBadItems] = useState( dataList.badItems, ); const [expiryItems, setExpiryItems] = useState( dataList.expiryItems, ); const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); const [submittingIds, setSubmittingIds] = useState>(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[] = 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( (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[]>( () => [ { 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) => ( ), }, ], [t, handleSubmitSingle, submittingIds, currentUserId], ); const badColumns = useMemo[]>( () => [ { 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) => ( ), }, ], [t, handleSubmitSingle, submittingIds, currentUserId], ); const expiryColumns = useMemo[]>( () => [ { 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) => ( ), }, ], [t, handleSubmitSingle, submittingIds, currentUserId], ); const handleSearch = useCallback(async (query: Record) => { 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 ( items={items} columns={missColumns} pagingController={paging} checkboxIds={selectedIds} setPagingController={setPaging} setCheckboxIds={setSelectedIds} /> ); } if (tab === "bad") { const items = filterBySearch(badItems); return ( items={items} columns={badColumns} pagingController={paging} setPagingController={setPaging} checkboxIds={selectedIds} setCheckboxIds={setSelectedIds} /> ); } const items = filterBySearch(expiryItems); return ( items={items} columns={expiryColumns} pagingController={paging} setPagingController={setPaging} checkboxIds={selectedIds} setCheckboxIds={setSelectedIds} /> ); }; return ( criteria={searchCriteria} onSearch={handleSearch} /> {tab === "expiry" && ( )} {renderCurrentTab()} setFormOpen(false)} lotId={selectedLotId} itemId={selectedItemId} issueType={selectedIssueType} currentUserId={currentUserId || 0} onSuccess={handleFormSuccess} /> ); }; export default SearchPage;