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.
 
 

429 lines
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 { createStockTransfer } 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<number | null>(null); // Store warehouse ID instead of code
  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(null);
  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(null);
  187. setTargetLocationInput("");
  188. setQtyToBeTransferred(0);
  189. }, []);
  190. const handleSubmitStockTransfer = useCallback(async () => {
  191. if (!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0) {
  192. return;
  193. }
  194. try {
  195. setIsUploading(true);
  196. const request = {
  197. inventoryLotLineId: selectedLotLine.id,
  198. transferredQty: qtyToBeTransferred,
  199. warehouseId: targetLocation, // targetLocation now contains warehouse ID
  200. };
  201. const response = await createStockTransfer(request);
  202. if (response && response.type === "success") {
  203. alert(t("Stock transfer successful"));
  204. handleCloseStockTransferModal();
  205. // Refresh the inventory lot lines list
  206. window.location.reload(); // Or use your preferred refresh method
  207. } else {
  208. throw new Error(response?.message || t("Failed to transfer stock"));
  209. }
  210. } catch (error: any) {
  211. console.error("Error transferring stock:", error);
  212. alert(error?.message || t("Failed to transfer stock. Please try again."));
  213. } finally {
  214. setIsUploading(false);
  215. }
  216. }, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t]);
  217. return <>
  218. <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography>
  219. <SearchResults<InventoryLotLineResult>
  220. items={inventoryLotLines ?? []}
  221. columns={columns}
  222. pagingController={pagingController}
  223. setPagingController={setPagingController}
  224. totalCount={totalCount}
  225. />
  226. <Modal
  227. open={stockTransferModalOpen}
  228. onClose={handleCloseStockTransferModal}
  229. sx={{
  230. display: 'flex',
  231. alignItems: 'center',
  232. justifyContent: 'center',
  233. }}
  234. >
  235. <Card
  236. sx={{
  237. position: 'relative',
  238. width: '95%',
  239. maxWidth: '1200px',
  240. maxHeight: '90vh',
  241. overflow: 'auto',
  242. p: 3,
  243. }}
  244. >
  245. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  246. <Typography variant="h6">
  247. {inventory && selectedLotLine
  248. ? `${inventory.itemCode} ${inventory.itemName} (${selectedLotLine.lotNo})`
  249. : t("Stock Transfer")
  250. }
  251. </Typography>
  252. <IconButton onClick={handleCloseStockTransferModal}>
  253. <CloseIcon />
  254. </IconButton>
  255. </Box>
  256. <Grid container spacing={1} sx={{ mt: 2 }}>
  257. <Grid item xs={5.5}>
  258. <TextField
  259. label={t("Start Location")}
  260. fullWidth
  261. variant="outlined"
  262. value={startLocation}
  263. disabled
  264. InputLabelProps={{
  265. shrink: !!startLocation,
  266. sx: { fontSize: "0.9375rem" },
  267. }}
  268. />
  269. </Grid>
  270. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  271. <Typography variant="body1">{t("to")}</Typography>
  272. </Grid>
  273. <Grid item xs={5.5}>
  274. <Autocomplete
  275. options={warehouses.filter(w => w.code !== startLocation)}
  276. getOptionLabel={(option) => option.code || ""}
  277. value={targetLocation ? warehouses.find(w => w.id === targetLocation) || null : null}
  278. inputValue={targetLocationInput}
  279. onInputChange={(event, newInputValue) => {
  280. setTargetLocationInput(newInputValue);
  281. if (targetLocation && newInputValue !== warehouses.find(w => w.id === targetLocation)?.code) {
  282. setTargetLocation(null);
  283. }
  284. }}
  285. onChange={(event, newValue) => {
  286. if (newValue) {
  287. setTargetLocation(newValue.id);
  288. setTargetLocationInput(newValue.code);
  289. } else {
  290. setTargetLocation(null);
  291. setTargetLocationInput("");
  292. }
  293. }}
  294. filterOptions={(options, { inputValue }) => {
  295. if (!inputValue || inputValue.trim() === "") return options;
  296. const searchTerm = inputValue.toLowerCase().trim();
  297. return options.filter((option) =>
  298. (option.code || "").toLowerCase().includes(searchTerm) ||
  299. (option.name || "").toLowerCase().includes(searchTerm) ||
  300. (option.description || "").toLowerCase().includes(searchTerm)
  301. );
  302. }}
  303. isOptionEqualToValue={(option, value) => option.id === value.id}
  304. autoHighlight={false}
  305. autoSelect={false}
  306. clearOnBlur={false}
  307. renderOption={(props, option) => (
  308. <li {...props}>
  309. {option.code}
  310. </li>
  311. )}
  312. renderInput={(params) => (
  313. <TextField
  314. {...params}
  315. label={t("Target Location")}
  316. variant="outlined"
  317. fullWidth
  318. InputLabelProps={{
  319. shrink: !!targetLocation || !!targetLocationInput,
  320. sx: { fontSize: "0.9375rem" },
  321. }}
  322. />
  323. )}
  324. />
  325. </Grid>
  326. </Grid>
  327. <Grid container spacing={1} sx={{ mt: 2 }}>
  328. <Grid item xs={2}>
  329. <TextField
  330. label={t("Original Qty")}
  331. fullWidth
  332. variant="outlined"
  333. value={originalQty}
  334. disabled
  335. InputLabelProps={{
  336. shrink: true,
  337. sx: { fontSize: "0.9375rem" },
  338. }}
  339. />
  340. </Grid>
  341. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  342. <Typography variant="body1">-</Typography>
  343. </Grid>
  344. <Grid item xs={2}>
  345. <TextField
  346. label={t("Qty To Be Transferred")}
  347. fullWidth
  348. variant="outlined"
  349. type="number"
  350. value={qtyToBeTransferred}
  351. onChange={(e) => {
  352. const value = parseInt(e.target.value) || 0;
  353. const maxValue = Math.max(0, originalQty);
  354. setQtyToBeTransferred(Math.min(Math.max(0, value), maxValue));
  355. }}
  356. inputProps={{ min: 0, max: originalQty, step: 1 }}
  357. InputLabelProps={{
  358. shrink: true,
  359. sx: { fontSize: "0.9375rem" },
  360. }}
  361. />
  362. </Grid>
  363. <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  364. <Typography variant="body1">=</Typography>
  365. </Grid>
  366. <Grid item xs={2}>
  367. <TextField
  368. label={t("Remaining Qty")}
  369. fullWidth
  370. variant="outlined"
  371. value={remainingQty}
  372. disabled
  373. InputLabelProps={{
  374. shrink: true,
  375. sx: { fontSize: "0.9375rem" },
  376. }}
  377. />
  378. </Grid>
  379. <Grid item xs={2}>
  380. <TextField
  381. label={t("Stock UoM")}
  382. fullWidth
  383. variant="outlined"
  384. value={selectedLotLine?.uom || ""}
  385. disabled
  386. InputLabelProps={{
  387. shrink: true,
  388. sx: { fontSize: "0.9375rem" },
  389. }}
  390. />
  391. </Grid>
  392. <Grid item xs={2} sx={{ display: 'flex', alignItems: 'center' }}>
  393. <Button
  394. variant="contained"
  395. fullWidth
  396. sx={{
  397. height: '56px',
  398. fontSize: '0.9375rem',
  399. }}
  400. onClick={handleSubmitStockTransfer}
  401. disabled={!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0 || qtyToBeTransferred > originalQty}
  402. >
  403. {t("Submit")}
  404. </Button>
  405. </Grid>
  406. </Grid>
  407. </Card>
  408. </Modal>
  409. </>
  410. }
  411. export default InventoryLotLineTable;