FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

306 righe
9.4 KiB

  1. "use client";
  2. import SearchBox, { Criterion } from "../SearchBox";
  3. import { useCallback, useMemo, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import SearchResults, { Column } from "../SearchResults/index";
  6. import { SessionWithTokens } from "@/config/authConfig";
  7. import {
  8. batchSubmitBadItem,
  9. batchSubmitExpiryItem,
  10. batchSubmitMissItem,
  11. ExpiryItemResult,
  12. StockIssueLists,
  13. StockIssueResult,
  14. submitBadItem,
  15. submitExpiryItem,
  16. submitMissItem,
  17. } from "@/app/api/stockIssue/actions";
  18. import { Box, Button, Tab, Tabs } from "@mui/material";
  19. import { useSession } from "next-auth/react";
  20. interface Props {
  21. dataList: StockIssueLists;
  22. }
  23. type SearchQuery = {
  24. lotNo: string;
  25. };
  26. type SearchParamNames = keyof SearchQuery;
  27. const SearchPage: React.FC<Props> = ({ dataList }) => {
  28. const { t } = useTranslation("inventory");
  29. const [tab, setTab] = useState<"miss" | "bad" | "expiry">("miss");
  30. const [search, setSearch] = useState<SearchQuery>({ lotNo: "" });
  31. const { data: session } = useSession() as { data: SessionWithTokens | null };
  32. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  33. const [missItems, setMissItems] = useState<StockIssueResult[]>(
  34. dataList.missItems,
  35. );
  36. const [badItems, setBadItems] = useState<StockIssueResult[]>(
  37. dataList.badItems,
  38. );
  39. const [expiryItems, setExpiryItems] = useState<ExpiryItemResult[]>(
  40. dataList.expiryItems,
  41. );
  42. const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
  43. const [submittingIds, setSubmittingIds] = useState<Set<number>>(new Set());
  44. const [batchSubmitting, setBatchSubmitting] = useState(false);
  45. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  46. () => [
  47. {
  48. label: t("Lot No."),
  49. paramName: "lotNo",
  50. type: "text",
  51. },
  52. ],
  53. [t],
  54. );
  55. const filterBySearch = useCallback(
  56. <T extends { lotNo: string | null }>(items: T[]): T[] => {
  57. if (!search.lotNo) return items;
  58. const keyword = search.lotNo.toLowerCase();
  59. return items.filter(
  60. (i) => i.lotNo && i.lotNo.toLowerCase().includes(keyword),
  61. );
  62. },
  63. [search.lotNo],
  64. );
  65. const handleSubmitSingle = useCallback(
  66. async (id: number) => {
  67. if (!currentUserId) {
  68. alert(t("User ID is required"));
  69. return;
  70. }
  71. setSubmittingIds((prev) => new Set(prev).add(id));
  72. try {
  73. if (tab === "miss") {
  74. await submitMissItem(id, currentUserId);
  75. setMissItems((prev) => prev.filter((i) => i.id !== id));
  76. } else if (tab === "bad") {
  77. await submitBadItem(id, currentUserId);
  78. setBadItems((prev) => prev.filter((i) => i.id !== id));
  79. } else {
  80. await submitExpiryItem(id, currentUserId);
  81. setExpiryItems((prev) => prev.filter((i) => i.id !== id));
  82. }
  83. // Remove from selectedIds if it was selected
  84. setSelectedIds((prev) => prev.filter((selectedId) => selectedId !== id));
  85. } catch (error) {
  86. console.error("Failed to submit item:", error);
  87. alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`);
  88. } finally {
  89. setSubmittingIds((prev) => {
  90. const newSet = new Set(prev);
  91. newSet.delete(id);
  92. return newSet;
  93. });
  94. }
  95. },
  96. [tab, currentUserId, t],
  97. );
  98. const handleSubmitSelected = useCallback(async () => {
  99. if (!currentUserId) return;
  100. // Get all IDs from the current tab's filtered items
  101. let allIds: number[] = [];
  102. if (tab === "miss") {
  103. const items = filterBySearch(missItems);
  104. allIds = items.map((item) => item.id);
  105. } else if (tab === "bad") {
  106. const items = filterBySearch(badItems);
  107. allIds = items.map((item) => item.id);
  108. } else {
  109. const items = filterBySearch(expiryItems);
  110. allIds = items.map((item) => item.id);
  111. }
  112. if (allIds.length === 0) return;
  113. setBatchSubmitting(true);
  114. try {
  115. if (tab === "miss") {
  116. await batchSubmitMissItem(allIds, currentUserId);
  117. setMissItems((prev) => prev.filter((i) => !allIds.includes(i.id)));
  118. } else if (tab === "bad") {
  119. await batchSubmitBadItem(allIds, currentUserId);
  120. setBadItems((prev) => prev.filter((i) => !allIds.includes(i.id)));
  121. } else {
  122. await batchSubmitExpiryItem(allIds, currentUserId);
  123. setExpiryItems((prev) => prev.filter((i) => !allIds.includes(i.id)));
  124. }
  125. setSelectedIds([]);
  126. } catch (error) {
  127. console.error("Failed to submit selected items:", error);
  128. alert(`Failed to submit: ${error instanceof Error ? error.message : "Unknown error"}`);
  129. } finally {
  130. setBatchSubmitting(false);
  131. }
  132. }, [tab, currentUserId, missItems, badItems, expiryItems, filterBySearch]);
  133. const missColumns = useMemo<Column<StockIssueResult>[]>(
  134. () => [
  135. { name: "itemCode", label: t("Item Code") },
  136. { name: "itemDescription", label: t("Item") },
  137. { name: "lotNo", label: t("Lot No.") },
  138. { name: "storeLocation", label: t("Location") },
  139. { name: "missQty", label: t("Miss Qty") },
  140. {
  141. name: "id",
  142. label: t("Action"),
  143. renderCell: (item) => (
  144. <Button
  145. size="small"
  146. variant="contained"
  147. color="primary"
  148. onClick={() => handleSubmitSingle(item.id)}
  149. disabled={submittingIds.has(item.id) || !currentUserId}
  150. >
  151. {submittingIds.has(item.id) ? t("Processing...") : t("Looked")}
  152. </Button>
  153. ),
  154. },
  155. ],
  156. [t, handleSubmitSingle, submittingIds, currentUserId],
  157. );
  158. const badColumns = useMemo<Column<StockIssueResult>[]>(
  159. () => [
  160. { name: "itemCode", label: t("Item Code") },
  161. { name: "itemDescription", label: t("Item") },
  162. { name: "lotNo", label: t("Lot No.") },
  163. { name: "storeLocation", label: t("Location") },
  164. { name: "badItemQty", label: t("Defective Qty") },
  165. {
  166. name: "id",
  167. label: t("Action"),
  168. renderCell: (item) => (
  169. <Button
  170. size="small"
  171. variant="contained"
  172. color="primary"
  173. onClick={() => handleSubmitSingle(item.id)}
  174. disabled={submittingIds.has(item.id) || !currentUserId}
  175. >
  176. {submittingIds.has(item.id) ? t("Disposing...") : t("Disposed")}
  177. </Button>
  178. ),
  179. },
  180. ],
  181. [t, handleSubmitSingle, submittingIds, currentUserId],
  182. );
  183. const expiryColumns = useMemo<Column<ExpiryItemResult>[]>(
  184. () => [
  185. { name: "itemCode", label: t("Item Code") },
  186. { name: "itemDescription", label: t("Item") },
  187. { name: "lotNo", label: t("Lot No.") },
  188. { name: "storeLocation", label: t("Location") },
  189. { name: "expiryDate", label: t("Expiry Date") },
  190. { name: "remainingQty", label: t("Remaining Qty") },
  191. {
  192. name: "id",
  193. label: t("Action"),
  194. renderCell: (item) => (
  195. <Button
  196. size="small"
  197. variant="contained"
  198. color="primary"
  199. onClick={() => handleSubmitSingle(item.id)}
  200. disabled={submittingIds.has(item.id) || !currentUserId}
  201. >
  202. {submittingIds.has(item.id) ? t("Disposing...") : t("Disposed")}
  203. </Button>
  204. ),
  205. },
  206. ],
  207. [t, handleSubmitSingle, submittingIds, currentUserId],
  208. );
  209. const handleSearch = useCallback((query: Record<SearchParamNames, string>) => {
  210. setSearch(query);
  211. }, []);
  212. const handleTabChange = useCallback(
  213. (_: React.SyntheticEvent, value: string) => {
  214. setTab(value as "miss" | "bad" | "expiry");
  215. setSelectedIds([]);
  216. },
  217. [],
  218. );
  219. const renderCurrentTab = () => {
  220. if (tab === "miss") {
  221. const items = filterBySearch(missItems);
  222. return (
  223. <SearchResults<StockIssueResult>
  224. items={items}
  225. columns={missColumns}
  226. pagingController={{ pageNum: 1, pageSize: 10 }}
  227. checkboxIds={selectedIds}
  228. setCheckboxIds={setSelectedIds}
  229. />
  230. );
  231. }
  232. if (tab === "bad") {
  233. const items = filterBySearch(badItems);
  234. return (
  235. <SearchResults<StockIssueResult>
  236. items={items}
  237. columns={badColumns}
  238. pagingController={{ pageNum: 1, pageSize: 10 }}
  239. checkboxIds={selectedIds}
  240. setCheckboxIds={setSelectedIds}
  241. />
  242. );
  243. }
  244. const items = filterBySearch(expiryItems);
  245. return (
  246. <SearchResults<ExpiryItemResult>
  247. items={items}
  248. columns={expiryColumns}
  249. pagingController={{ pageNum: 1, pageSize: 10 }}
  250. checkboxIds={selectedIds}
  251. setCheckboxIds={setSelectedIds}
  252. />
  253. );
  254. };
  255. return (
  256. <Box>
  257. <Tabs value={tab} onChange={handleTabChange} sx={{ mb: 2 }}>
  258. <Tab value="miss" label={t("Miss Item")} />
  259. <Tab value="bad" label={t("Bad Item")} />
  260. <Tab value="expiry" label={t("Expiry Item")} />
  261. </Tabs>
  262. <SearchBox<SearchParamNames>
  263. criteria={searchCriteria}
  264. onSearch={handleSearch}
  265. />
  266. <Box sx={{ display: "flex", justifyContent: "flex-end", mb: 1 }}>
  267. <Button
  268. variant="contained"
  269. color="primary"
  270. onClick={handleSubmitSelected}
  271. disabled={batchSubmitting || !currentUserId}
  272. >
  273. {batchSubmitting ? tab === "miss" ? t("Processing...") : tab === "bad" ? t("Disposing...") : t("Disposing...") : t("Batch Disposed All")}
  274. </Button>
  275. </Box>
  276. {renderCurrentTab()}
  277. </Box>
  278. );
  279. };
  280. export default SearchPage;