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.
 
 

417 line
15 KiB

  1. import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory";
  2. import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
  3. import { useTranslation } from "react-i18next";
  4. import { Column } from "../SearchResults";
  5. import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults";
  6. import { arrayToDateString } from "@/app/utils/formatUtil";
  7. import { Box, Card, Grid, IconButton, Modal, TextField, Typography, Button } from "@mui/material";
  8. import useUploadContext from "../UploadProvider/useUploadContext";
  9. import { downloadFile } from "@/app/utils/commonUtil";
  10. import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions";
  11. import QrCodeIcon from "@mui/icons-material/QrCode";
  12. import PrintIcon from "@mui/icons-material/Print";
  13. import SwapHoriz from "@mui/icons-material/SwapHoriz";
  14. import CloseIcon from "@mui/icons-material/Close";
  15. import { Autocomplete } from "@mui/material";
  16. import { WarehouseResult } from "@/app/api/warehouse";
  17. import { fetchWarehouseListClient } from "@/app/api/warehouse/client";
  18. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  19. interface Props {
  20. inventoryLotLines: InventoryLotLineResult[] | null;
  21. setPagingController: defaultSetPagingController;
  22. pagingController: typeof defaultPagingController;
  23. totalCount: number;
  24. inventory: InventoryResult | null;
  25. }
  26. const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory }) => {
  27. const { t } = useTranslation(["inventory"]);
  28. const { setIsUploading } = useUploadContext();
  29. const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false);
  30. const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null);
  31. const [startLocation, setStartLocation] = useState<string>("");
  32. const [targetLocation, setTargetLocation] = useState<string>("");
  33. const [targetLocationInput, setTargetLocationInput] = useState<string>("");
  34. const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0);
  35. const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]);
  36. useEffect(() => {
  37. if (stockTransferModalOpen) {
  38. fetchWarehouseListClient()
  39. .then(setWarehouses)
  40. .catch(console.error);
  41. }
  42. }, [stockTransferModalOpen]);
  43. const originalQty = selectedLotLine?.availableQty || 0;
  44. const remainingQty = originalQty - qtyToBeTransferred;
  45. const downloadQrCode = useCallback(async (lotLineId: number) => {
  46. setIsUploading(true);
  47. // const postData = { stockInLineIds: [42,43,44] };
  48. const postData: LotLineToQrcode = {
  49. inventoryLotLineId: lotLineId
  50. }
  51. const response = await fetchQrCodeByLotLineId(postData);
  52. if (response) {
  53. downloadFile(new Uint8Array(response.blobValue), response.filename!);
  54. }
  55. setIsUploading(false);
  56. }, [setIsUploading]);
  57. const handleStockTransfer = useCallback(
  58. (lotLine: InventoryLotLineResult) => {
  59. setSelectedLotLine(lotLine);
  60. setStockTransferModalOpen(true);
  61. setStartLocation(lotLine.warehouse.code || "");
  62. setTargetLocation("");
  63. setTargetLocationInput("");
  64. setQtyToBeTransferred(0);
  65. },
  66. [],
  67. );
  68. const onDetailClick = useCallback(
  69. (lotLine: InventoryLotLineResult) => {
  70. downloadQrCode(lotLine.id)
  71. // lot line id to find stock in line
  72. },
  73. [downloadQrCode],
  74. );
  75. const columns = useMemo<Column<InventoryLotLineResult>[]>(
  76. () => [
  77. // {
  78. // name: "item",
  79. // label: t("Code"),
  80. // renderCell: (params) => {
  81. // return params.item.code;
  82. // },
  83. // },
  84. // {
  85. // name: "item",
  86. // label: t("Name"),
  87. // renderCell: (params) => {
  88. // return params.item.name;
  89. // },
  90. // },
  91. {
  92. name: "lotNo",
  93. label: t("Lot No"),
  94. },
  95. // {
  96. // name: "item",
  97. // label: t("Type"),
  98. // renderCell: (params) => {
  99. // return t(params.item.type);
  100. // },
  101. // },
  102. {
  103. name: "availableQty",
  104. label: t("Available Qty"),
  105. align: "right",
  106. headerAlign: "right",
  107. type: "integer",
  108. },
  109. {
  110. name: "uom",
  111. label: t("Stock UoM"),
  112. align: "left",
  113. headerAlign: "left",
  114. },
  115. // {
  116. // name: "qtyPerSmallestUnit",
  117. // label: t("Available Qty Per Smallest Unit"),
  118. // align: "right",
  119. // headerAlign: "right",
  120. // type: "integer",
  121. // },
  122. // {
  123. // name: "baseUom",
  124. // label: t("Base UoM"),
  125. // align: "left",
  126. // headerAlign: "left",
  127. // },
  128. {
  129. name: "expiryDate",
  130. label: t("Expiry Date"),
  131. renderCell: (params) => {
  132. return arrayToDateString(params.expiryDate)
  133. },
  134. },
  135. {
  136. name: "warehouse",
  137. label: t("Warehouse"),
  138. renderCell: (params) => {
  139. return `${params.warehouse.code}`
  140. },
  141. },
  142. {
  143. name: "id",
  144. label: t("Download QR Code"),
  145. onClick: onDetailClick,
  146. buttonIcon: <QrCodeIcon />,
  147. align: "center",
  148. headerAlign: "center",
  149. },
  150. {
  151. name: "id",
  152. label: t("Print QR Code"),
  153. onClick: () => {},
  154. buttonIcon: <PrintIcon />,
  155. align: "center",
  156. headerAlign: "center",
  157. },
  158. {
  159. name: "id",
  160. label: t("Stock Transfer"),
  161. onClick: handleStockTransfer,
  162. buttonIcon: <SwapHoriz />,
  163. align: "center",
  164. headerAlign: "center",
  165. },
  166. // {
  167. // name: "status",
  168. // label: t("Status"),
  169. // type: "icon",
  170. // icons: {
  171. // available: <CheckCircleOutline fontSize="small"/>,
  172. // unavailable: <DoDisturb fontSize="small"/>,
  173. // },
  174. // colors: {
  175. // available: "success",
  176. // unavailable: "error",
  177. // }
  178. // },
  179. ],
  180. [t, onDetailClick, downloadQrCode, handleStockTransfer],
  181. );
  182. const handleCloseStockTransferModal = useCallback(() => {
  183. setStockTransferModalOpen(false);
  184. setSelectedLotLine(null);
  185. setStartLocation("");
  186. setTargetLocation("");
  187. setTargetLocationInput("");
  188. setQtyToBeTransferred(0);
  189. }, []);
  190. const handleSubmitStockTransfer = useCallback(async () => {
  191. try {
  192. setIsUploading(true);
  193. // Decrease the inQty (availableQty) in the source inventory lot line
  194. // TODO: Add logic to increase qty in target location warehouse
  195. alert(t("Stock transfer successful"));
  196. handleCloseStockTransferModal();
  197. // TODO: Refresh the inventory lot lines list
  198. } catch (error: any) {
  199. console.error("Error transferring stock:", error);
  200. alert(error?.message || t("Failed to transfer stock. Please try again."));
  201. } finally {
  202. setIsUploading(false);
  203. }
  204. }, [selectedLotLine, targetLocation, qtyToBeTransferred, originalQty, handleCloseStockTransferModal, setIsUploading, t]);
  205. return <>
  206. <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography>
  207. <SearchResults<InventoryLotLineResult>
  208. items={inventoryLotLines ?? []}
  209. columns={columns}
  210. pagingController={pagingController}
  211. setPagingController={setPagingController}
  212. totalCount={totalCount}
  213. />
  214. <Modal
  215. open={stockTransferModalOpen}
  216. onClose={handleCloseStockTransferModal}
  217. sx={{
  218. display: 'flex',
  219. alignItems: 'center',
  220. justifyContent: 'center',
  221. }}
  222. >
  223. <Card
  224. sx={{
  225. position: 'relative',
  226. width: '95%',
  227. maxWidth: '1200px',
  228. maxHeight: '90vh',
  229. overflow: 'auto',
  230. p: 3,
  231. }}
  232. >
  233. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  234. <Typography variant="h6">
  235. {inventory && selectedLotLine
  236. ? `${inventory.itemCode} ${inventory.itemName} (${selectedLotLine.lotNo})`
  237. : t("Stock Transfer")
  238. }
  239. </Typography>
  240. <IconButton onClick={handleCloseStockTransferModal}>
  241. <CloseIcon />
  242. </IconButton>
  243. </Box>
  244. <Grid container spacing={1} sx={{ mt: 2 }}>
  245. <Grid item xs={5.5}>
  246. <TextField
  247. label={t("Start Location")}
  248. fullWidth
  249. variant="outlined"
  250. value={startLocation}
  251. disabled
  252. InputLabelProps={{
  253. shrink: !!startLocation,
  254. sx: { fontSize: "0.9375rem" },
  255. }}
  256. />
  257. </Grid>
  258. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  259. <Typography variant="body1">{t("to")}</Typography>
  260. </Grid>
  261. <Grid item xs={5.5}>
  262. <Autocomplete
  263. options={warehouses.filter(w => w.code !== startLocation)}
  264. getOptionLabel={(option) => option.code || ""}
  265. value={targetLocation ? warehouses.find(w => w.code === targetLocation) || null : null}
  266. inputValue={targetLocationInput}
  267. onInputChange={(event, newInputValue) => {
  268. setTargetLocationInput(newInputValue);
  269. if (targetLocation && newInputValue !== targetLocation) {
  270. setTargetLocation("");
  271. }
  272. }}
  273. onChange={(event, newValue) => {
  274. if (newValue) {
  275. setTargetLocation(newValue.code);
  276. setTargetLocationInput(newValue.code);
  277. } else {
  278. setTargetLocation("");
  279. setTargetLocationInput("");
  280. }
  281. }}
  282. filterOptions={(options, { inputValue }) => {
  283. if (!inputValue || inputValue.trim() === "") return options;
  284. const searchTerm = inputValue.toLowerCase().trim();
  285. return options.filter((option) =>
  286. (option.code || "").toLowerCase().includes(searchTerm) ||
  287. (option.name || "").toLowerCase().includes(searchTerm) ||
  288. (option.description || "").toLowerCase().includes(searchTerm)
  289. );
  290. }}
  291. isOptionEqualToValue={(option, value) => option.code === value.code}
  292. autoHighlight={false}
  293. autoSelect={false}
  294. clearOnBlur={false}
  295. renderOption={(props, option) => (
  296. <li {...props}>
  297. {option.code}
  298. </li>
  299. )}
  300. renderInput={(params) => (
  301. <TextField
  302. {...params}
  303. label={t("Target Location")}
  304. variant="outlined"
  305. fullWidth
  306. InputLabelProps={{
  307. shrink: !!targetLocation || !!targetLocationInput,
  308. sx: { fontSize: "0.9375rem" },
  309. }}
  310. />
  311. )}
  312. />
  313. </Grid>
  314. </Grid>
  315. <Grid container spacing={1} sx={{ mt: 2 }}>
  316. <Grid item xs={2}>
  317. <TextField
  318. label={t("Original Qty")}
  319. fullWidth
  320. variant="outlined"
  321. value={originalQty}
  322. disabled
  323. InputLabelProps={{
  324. shrink: true,
  325. sx: { fontSize: "0.9375rem" },
  326. }}
  327. />
  328. </Grid>
  329. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  330. <Typography variant="body1">-</Typography>
  331. </Grid>
  332. <Grid item xs={2}>
  333. <TextField
  334. label={t("Qty To Be Transferred")}
  335. fullWidth
  336. variant="outlined"
  337. type="number"
  338. value={qtyToBeTransferred}
  339. onChange={(e) => {
  340. const value = parseInt(e.target.value) || 0;
  341. const maxValue = Math.max(0, originalQty);
  342. setQtyToBeTransferred(Math.min(Math.max(0, value), maxValue));
  343. }}
  344. inputProps={{ min: 0, max: originalQty, step: 1 }}
  345. InputLabelProps={{
  346. shrink: true,
  347. sx: { fontSize: "0.9375rem" },
  348. }}
  349. />
  350. </Grid>
  351. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  352. <Typography variant="body1">=</Typography>
  353. </Grid>
  354. <Grid item xs={2}>
  355. <TextField
  356. label={t("Remaining Qty")}
  357. fullWidth
  358. variant="outlined"
  359. value={remainingQty}
  360. disabled
  361. InputLabelProps={{
  362. shrink: true,
  363. sx: { fontSize: "0.9375rem" },
  364. }}
  365. />
  366. </Grid>
  367. <Grid item xs={2}>
  368. <TextField
  369. label={t("Stock UoM")}
  370. fullWidth
  371. variant="outlined"
  372. value={selectedLotLine?.uom || ""}
  373. disabled
  374. InputLabelProps={{
  375. shrink: true,
  376. sx: { fontSize: "0.9375rem" },
  377. }}
  378. />
  379. </Grid>
  380. <Grid item xs={2} sx={{ display: 'flex', alignItems: 'center' }}>
  381. <Button
  382. variant="contained"
  383. fullWidth
  384. sx={{
  385. height: '56px',
  386. fontSize: '0.9375rem',
  387. }}
  388. onClick={handleSubmitStockTransfer}
  389. disabled={!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0 || qtyToBeTransferred > originalQty}
  390. >
  391. {t("Submit")}
  392. </Button>
  393. </Grid>
  394. </Grid>
  395. </Card>
  396. </Modal>
  397. </>
  398. }
  399. export default InventoryLotLineTable;