FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

231 lines
6.7 KiB

  1. "use client";
  2. import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory";
  3. import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions";
  4. import { arrayToDateString } from "@/app/utils/formatUtil";
  5. import { msg, msgError } from "@/components/Swal/CustomAlerts";
  6. import HighlightOffIcon from "@mui/icons-material/HighlightOff";
  7. import {
  8. Box,
  9. FormControl,
  10. IconButton,
  11. InputLabel,
  12. MenuItem,
  13. Select,
  14. SelectChangeEvent,
  15. Typography,
  16. } from "@mui/material";
  17. import { useCallback, useMemo, useRef, useState } from "react";
  18. import { useTranslation } from "react-i18next";
  19. import { Column } from "../SearchResults";
  20. import SearchResults, {
  21. defaultPagingController,
  22. defaultSetPagingController,
  23. } from "../SearchResults/SearchResults";
  24. import BadItemHandleModal from "./BadItemHandleModal";
  25. const LOT_STATUSES = ["available", "unavailable"] as const;
  26. interface Props {
  27. inventoryLotLines: InventoryLotLineResult[] | null;
  28. setPagingController: defaultSetPagingController;
  29. pagingController: typeof defaultPagingController;
  30. totalCount: number;
  31. inventory: InventoryResult | null;
  32. currentUserId?: number;
  33. onBadItemHandleSuccess?: (payload: {
  34. inventoryLotLineId: number;
  35. qty: number;
  36. }) => void | Promise<void>;
  37. onLotLinesChanged?: () => void | Promise<void>;
  38. }
  39. const StockIssueLotLineTable: React.FC<Props> = ({
  40. inventoryLotLines,
  41. pagingController,
  42. setPagingController,
  43. totalCount,
  44. inventory,
  45. currentUserId,
  46. onBadItemHandleSuccess,
  47. onLotLinesChanged,
  48. }) => {
  49. const { t } = useTranslation(["stockIssue", "common"]);
  50. const [modalOpen, setModalOpen] = useState(false);
  51. const [selectedLotLine, setSelectedLotLine] =
  52. useState<InventoryLotLineResult | null>(null);
  53. const [statusUpdatingIds, setStatusUpdatingIds] = useState<Set<number>>(
  54. new Set(),
  55. );
  56. const statusInFlightRef = useRef<Set<number>>(new Set());
  57. const displayLotLines = useMemo(
  58. () => inventoryLotLines ?? [],
  59. [inventoryLotLines],
  60. );
  61. const isBadItemEnabled = useCallback((line: InventoryLotLineResult) => {
  62. const qty = line.availableQty ?? 0;
  63. return qty > 0;
  64. }, []);
  65. const handleBadItemClick = useCallback((lotLine: InventoryLotLineResult) => {
  66. if (!isBadItemEnabled(lotLine)) return;
  67. setSelectedLotLine(lotLine);
  68. setModalOpen(true);
  69. }, [isBadItemEnabled]);
  70. const handleStatusChange = useCallback(
  71. async (line: InventoryLotLineResult, event: SelectChangeEvent<string>) => {
  72. const nextStatus = event.target.value;
  73. if (!nextStatus || nextStatus === line.status) return;
  74. if (statusInFlightRef.current.has(line.id)) return;
  75. statusInFlightRef.current.add(line.id);
  76. setStatusUpdatingIds((prev) => new Set(prev).add(line.id));
  77. try {
  78. const res = await updateInventoryLotLineStatus({
  79. inventoryLotLineId: line.id,
  80. status: nextStatus,
  81. });
  82. if (res?.code && res.code !== "SUCCESS") {
  83. throw new Error(res.message ?? t("Failed to submit"));
  84. }
  85. msg(t("Saved successfully"));
  86. await onLotLinesChanged?.();
  87. } catch (e: unknown) {
  88. msgError(e instanceof Error ? e.message : t("Failed to submit"));
  89. } finally {
  90. statusInFlightRef.current.delete(line.id);
  91. setStatusUpdatingIds((prev) => {
  92. const next = new Set(prev);
  93. next.delete(line.id);
  94. return next;
  95. });
  96. }
  97. },
  98. [t, onLotLinesChanged],
  99. );
  100. const formatStatusLabel = useCallback(
  101. (status: string) => {
  102. const key = status?.toLowerCase();
  103. if (key === "available") return t("available");
  104. if (key === "unavailable") return t("unavailable");
  105. return status;
  106. },
  107. [t],
  108. );
  109. const columns = useMemo<Column<InventoryLotLineResult>[]>(
  110. () => [
  111. { name: "lotNo", label: t("Lot No") },
  112. {
  113. name: "availableQty",
  114. label: t("Available Qty"),
  115. align: "right",
  116. headerAlign: "right",
  117. type: "integer",
  118. },
  119. { name: "uom", label: t("Stock UoM") },
  120. {
  121. name: "expiryDate",
  122. label: t("Expiry Date"),
  123. renderCell: (params) => arrayToDateString(params.expiryDate),
  124. },
  125. {
  126. name: "warehouse",
  127. label: t("Warehouse"),
  128. renderCell: (params) => params.warehouse?.code ?? "",
  129. },
  130. {
  131. name: "status",
  132. label: t("Status"),
  133. renderCell: (row) => (
  134. <FormControl
  135. size="small"
  136. fullWidth
  137. disabled={statusUpdatingIds.has(row.id)}
  138. >
  139. <InputLabel id={`lot-status-${row.id}`}>{t("Status")}</InputLabel>
  140. <Select
  141. labelId={`lot-status-${row.id}`}
  142. label={t("Status")}
  143. value={
  144. LOT_STATUSES.includes(
  145. row.status?.toLowerCase() as (typeof LOT_STATUSES)[number],
  146. )
  147. ? row.status!.toLowerCase()
  148. : "unavailable"
  149. }
  150. onChange={(e) => handleStatusChange(row, e)}
  151. >
  152. {LOT_STATUSES.map((s) => (
  153. <MenuItem key={s} value={s}>
  154. {formatStatusLabel(s)}
  155. </MenuItem>
  156. ))}
  157. </Select>
  158. </FormControl>
  159. ),
  160. },
  161. {
  162. name: "id",
  163. label: t("Bad Item Handle"),
  164. align: "center",
  165. headerAlign: "center",
  166. renderCell: (row) => (
  167. <IconButton
  168. color="error"
  169. disabled={!isBadItemEnabled(row) || !currentUserId}
  170. onClick={() => handleBadItemClick(row)}
  171. title={t("Bad Item Handle")}
  172. >
  173. <HighlightOffIcon />
  174. </IconButton>
  175. ),
  176. },
  177. ],
  178. [
  179. t,
  180. handleStatusChange,
  181. formatStatusLabel,
  182. statusUpdatingIds,
  183. isBadItemEnabled,
  184. handleBadItemClick,
  185. currentUserId,
  186. ],
  187. );
  188. return (
  189. <>
  190. <Box sx={{ mb: 2 }}>
  191. <Typography variant="h6">
  192. {inventory
  193. ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType, { ns: "common", defaultValue: inventory.itemType })})`
  194. : t("No items are selected yet.")}
  195. </Typography>
  196. </Box>
  197. <SearchResults<InventoryLotLineResult>
  198. items={displayLotLines}
  199. columns={columns}
  200. pagingController={pagingController}
  201. setPagingController={setPagingController}
  202. totalCount={totalCount}
  203. />
  204. <BadItemHandleModal
  205. open={modalOpen}
  206. onClose={() => setModalOpen(false)}
  207. lotLine={selectedLotLine}
  208. inventory={inventory}
  209. currentUserId={currentUserId}
  210. onSuccess={async (payload) => {
  211. await onBadItemHandleSuccess?.(payload);
  212. }}
  213. />
  214. </>
  215. );
  216. };
  217. export default StockIssueLotLineTable;