From 6aefd923c5d258fb6c5821db6a802b6db903b28e Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 16 Jan 2026 11:44:43 +0800 Subject: [PATCH] updatestock issue --- src/app/(main)/stockIssue/page.tsx | 6 +- src/components/StockIssue/SearchPage.tsx | 307 ++++++++++++++++++++--- src/components/StockIssue/action.ts | 50 ++-- src/components/StockIssue/index.tsx | 4 +- 4 files changed, 299 insertions(+), 68 deletions(-) diff --git a/src/app/(main)/stockIssue/page.tsx b/src/app/(main)/stockIssue/page.tsx index e8cc375..973ff8c 100644 --- a/src/app/(main)/stockIssue/page.tsx +++ b/src/app/(main)/stockIssue/page.tsx @@ -7,17 +7,17 @@ import { Metadata } from "next"; import { Suspense } from "react"; export const metadata: Metadata = { - title: "Pick Order", + title: "Stock Issue", }; const SearchView: React.FC = async () => { - const { t } = await getServerI18n("pickOrder"); + const { t } = await getServerI18n("inventory"); PreloadList(); return ( <> - + }> diff --git a/src/components/StockIssue/SearchPage.tsx b/src/components/StockIssue/SearchPage.tsx index af37375..608dc31 100644 --- a/src/components/StockIssue/SearchPage.tsx +++ b/src/components/StockIssue/SearchPage.tsx @@ -4,53 +4,302 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; -import { StockIssueResult } from "./action"; +import { SessionWithTokens } from "@/config/authConfig"; +import { + batchSubmitBadItem, + batchSubmitExpiryItem, + batchSubmitMissItem, + ExpiryItemResult, + StockIssueLists, + StockIssueResult, + submitBadItem, + submitExpiryItem, + submitMissItem, +} from "@/app/api/stockIssue/actions"; +import { Box, Button, Tab, Tabs } from "@mui/material"; +import { useSession } from "next-auth/react"; interface Props { - dataList: StockIssueResult[]; + dataList: StockIssueLists; +} + +type SearchQuery = { + lotNo: string; }; -type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; -const SearchPage: React.FC = ({dataList}) => { - const { t } = useTranslation("user"); - const [filteredList, setFilteredList] = useState(dataList); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Lot No."), - paramName: "lotNo", - type: "text", - }, - ], - [t], +const SearchPage: React.FC = ({ dataList }) => { + const { t } = useTranslation("inventory"); + const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss"); + const [search, setSearch] = useState({ lotNo: "" }); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + const currentUserId = session?.id ? parseInt(session.id) : undefined; + 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 searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Lot No."), + paramName: "lotNo", + type: "text", + }, + ], + [t], + ); + + 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; + } - const columns = useMemo[]>( + setSubmittingIds((prev) => new Set(prev).add(id)); + try { + if (tab === "miss") { + await submitMissItem(id, currentUserId); + setMissItems((prev) => prev.filter((i) => i.id !== id)); + } else if (tab === "bad") { + await submitBadItem(id, currentUserId); + setBadItems((prev) => prev.filter((i) => i.id !== id)); + } else { + await submitExpiryItem(id, currentUserId); + setExpiryItems((prev) => prev.filter((i) => i.id !== id)); + } + // Remove from selectedIds if it was selected + setSelectedIds((prev) => prev.filter((selectedId) => selectedId !== id)); + } catch (error) { + console.error("Failed to submit item:", error); + alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); + } finally { + setSubmittingIds((prev) => { + const newSet = new Set(prev); + newSet.delete(id); + return newSet; + }); + } + }, + [tab, currentUserId, t], + ); + + 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); + try { + if (tab === "miss") { + await batchSubmitMissItem(allIds, currentUserId); + setMissItems((prev) => prev.filter((i) => !allIds.includes(i.id))); + } else if (tab === "bad") { + await batchSubmitBadItem(allIds, currentUserId); + setBadItems((prev) => prev.filter((i) => !allIds.includes(i.id))); + } else { + await batchSubmitExpiryItem(allIds, currentUserId); + setExpiryItems((prev) => prev.filter((i) => !allIds.includes(i.id))); + } + + setSelectedIds([]); + } catch (error) { + console.error("Failed to submit selected items:", error); + alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`); + } finally { + setBatchSubmitting(false); + } + }, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch]); + + 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: "badItemQty", label: t("Defective Qty") } + { name: "missQty", label: t("Miss Qty") }, + { + name: "id", + label: t("Action"), + renderCell: (item) => ( + + ), + }, ], - [t], + [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: "badItemQty", label: t("Defective Qty") }, + { + 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") }, + { name: "remainingQty", label: t("Remaining Qty") }, + { + name: "id", + label: t("Action"), + renderCell: (item) => ( + + ), + }, + ], + [t, handleSubmitSingle, submittingIds, currentUserId], + ); + + const handleSearch = useCallback((query: Record) => { + setSearch(query); + }, []); + + const handleTabChange = useCallback( + (_: React.SyntheticEvent, value: string) => { + setTab(value as "miss" | "bad" | "expiry"); + setSelectedIds([]); + }, + [], ); + const renderCurrentTab = () => { + if (tab === "miss") { + const items = filterBySearch(missItems); + return ( + + items={items} + columns={missColumns} + pagingController={{ pageNum: 1, pageSize: 10 }} + checkboxIds={selectedIds} + setCheckboxIds={setSelectedIds} + /> + ); + } + + if (tab === "bad") { + const items = filterBySearch(badItems); + return ( + + items={items} + columns={badColumns} + pagingController={{ pageNum: 1, pageSize: 10 }} + checkboxIds={selectedIds} + setCheckboxIds={setSelectedIds} + /> + ); + } + + const items = filterBySearch(expiryItems); + return ( + + items={items} + columns={expiryColumns} + pagingController={{ pageNum: 1, pageSize: 10 }} + checkboxIds={selectedIds} + setCheckboxIds={setSelectedIds} + /> + ); + }; + return ( - <> - + + + + + + + criteria={searchCriteria} - onSearch={(query) => { - }} - /> - - items={filteredList} - columns={columns} - pagingController={{ pageNum: 1, pageSize: 10 }} + onSearch={handleSearch} /> - + + + + + + {renderCurrentTab()} + ); }; diff --git a/src/components/StockIssue/action.ts b/src/components/StockIssue/action.ts index 7c2a0b0..6cb832b 100644 --- a/src/components/StockIssue/action.ts +++ b/src/components/StockIssue/action.ts @@ -1,33 +1,17 @@ -import "server-only"; - -import { BASE_API_URL } from "@/config/api"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; -import { cache } from "react"; - -export interface StockIssueResult { - action: any; - id: number; - itemId: number; - itemCode: string; - itemDescription: string; - lotId: number; - lotNo: string; - storeLocation: string; - requiredQty: number; - badItemQty: number; - issueRemark: string; - pickerName: string; - handleStatus: string; - handleDate: string; - handledBy: number; -} - -export const PreloadList = () => { - fetchList(); -}; - -export const fetchList = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/pickExecution/badItemList`, { - next: { tags: ["Bad Item List"] }, - }); -}); +// Re-export types and functions from the main actions file +export { + type StockIssueResult, + type ExpiryItemResult, + type StockIssueLists, + fetchList, + fetchMissItemList, + fetchBadItemList, + fetchExpiryItemList, + submitMissItem, + submitBadItem, + submitExpiryItem, + batchSubmitMissItem, + batchSubmitBadItem, + batchSubmitExpiryItem, + PreloadList, +} from "@/app/api/stockIssue/actions"; diff --git a/src/components/StockIssue/index.tsx b/src/components/StockIssue/index.tsx index 7dc47a9..fdf22b6 100644 --- a/src/components/StockIssue/index.tsx +++ b/src/components/StockIssue/index.tsx @@ -1,7 +1,6 @@ import GeneralLoading from "../General/GeneralLoading"; import SearchPage from "./SearchPage"; -import { fetchList } from "./action"; - +import { fetchList } from "@/app/api/stockIssue/actions"; interface SubComponents { Loading: typeof GeneralLoading; @@ -9,7 +8,6 @@ interface SubComponents { const Wrapper: React.FC & SubComponents = async () => { const dataList = await fetchList(); - console.log(dataList); return ; };