| @@ -1,16 +1,21 @@ | |||||
| import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; | import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; | ||||
| import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; | |||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { Column } from "../SearchResults"; | import { Column } from "../SearchResults"; | ||||
| import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; | import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; | ||||
| import { CheckCircleOutline, DoDisturb, EditNote } from "@mui/icons-material"; | |||||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | import { arrayToDateString } from "@/app/utils/formatUtil"; | ||||
| import { Typography } from "@mui/material"; | |||||
| import { isFinite } from "lodash"; | |||||
| import { Box, Card, Grid, IconButton, Modal, TextField, Typography, Button } from "@mui/material"; | |||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | import useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
| import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions"; | import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions"; | ||||
| import QrCodeIcon from "@mui/icons-material/QrCode"; | import QrCodeIcon from "@mui/icons-material/QrCode"; | ||||
| import PrintIcon from "@mui/icons-material/Print"; | |||||
| import SwapHoriz from "@mui/icons-material/SwapHoriz"; | |||||
| import CloseIcon from "@mui/icons-material/Close"; | |||||
| import { Autocomplete } from "@mui/material"; | |||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; | |||||
| import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; | |||||
| interface Props { | interface Props { | ||||
| inventoryLotLines: InventoryLotLineResult[] | null; | inventoryLotLines: InventoryLotLineResult[] | null; | ||||
| @@ -23,8 +28,26 @@ interface Props { | |||||
| const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory }) => { | const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory }) => { | ||||
| const { t } = useTranslation(["inventory"]); | const { t } = useTranslation(["inventory"]); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); | |||||
| const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null); | |||||
| const [startLocation, setStartLocation] = useState<string>(""); | |||||
| const [targetLocation, setTargetLocation] = useState<string>(""); | |||||
| const [targetLocationInput, setTargetLocationInput] = useState<string>(""); | |||||
| const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0); | |||||
| const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]); | |||||
| const printQrcode = useCallback(async (lotLineId: number) => { | |||||
| useEffect(() => { | |||||
| if (stockTransferModalOpen) { | |||||
| fetchWarehouseListClient() | |||||
| .then(setWarehouses) | |||||
| .catch(console.error); | |||||
| } | |||||
| }, [stockTransferModalOpen]); | |||||
| const originalQty = selectedLotLine?.availableQty || 0; | |||||
| const remainingQty = originalQty - qtyToBeTransferred; | |||||
| const downloadQrCode = useCallback(async (lotLineId: number) => { | |||||
| setIsUploading(true); | setIsUploading(true); | ||||
| // const postData = { stockInLineIds: [42,43,44] }; | // const postData = { stockInLineIds: [42,43,44] }; | ||||
| const postData: LotLineToQrcode = { | const postData: LotLineToQrcode = { | ||||
| @@ -37,12 +60,24 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| setIsUploading(false); | setIsUploading(false); | ||||
| }, [setIsUploading]); | }, [setIsUploading]); | ||||
| const handleStockTransfer = useCallback( | |||||
| (lotLine: InventoryLotLineResult) => { | |||||
| setSelectedLotLine(lotLine); | |||||
| setStockTransferModalOpen(true); | |||||
| setStartLocation(lotLine.warehouse.code || ""); | |||||
| setTargetLocation(""); | |||||
| setTargetLocationInput(""); | |||||
| setQtyToBeTransferred(0); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onDetailClick = useCallback( | const onDetailClick = useCallback( | ||||
| (lotLine: InventoryLotLineResult) => { | (lotLine: InventoryLotLineResult) => { | ||||
| printQrcode(lotLine.id) | |||||
| downloadQrCode(lotLine.id) | |||||
| // lot line id to find stock in line | // lot line id to find stock in line | ||||
| }, | }, | ||||
| [printQrcode], | |||||
| [downloadQrCode], | |||||
| ); | ); | ||||
| const columns = useMemo<Column<InventoryLotLineResult>[]>( | const columns = useMemo<Column<InventoryLotLineResult>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -108,14 +143,32 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| name: "warehouse", | name: "warehouse", | ||||
| label: t("Warehouse"), | label: t("Warehouse"), | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| return `${params.warehouse.code} - ${params.warehouse.name}` | |||||
| return `${params.warehouse.code}` | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| label: t("qrcode"), | |||||
| label: t("Download QR Code"), | |||||
| onClick: onDetailClick, | onClick: onDetailClick, | ||||
| buttonIcon: <QrCodeIcon />, | buttonIcon: <QrCodeIcon />, | ||||
| align: "center", | |||||
| headerAlign: "center", | |||||
| }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Print QR Code"), | |||||
| onClick: () => {}, | |||||
| buttonIcon: <PrintIcon />, | |||||
| align: "center", | |||||
| headerAlign: "center", | |||||
| }, | |||||
| { | |||||
| name: "id", | |||||
| label: t("Stock Transfer"), | |||||
| onClick: handleStockTransfer, | |||||
| buttonIcon: <SwapHoriz />, | |||||
| align: "center", | |||||
| headerAlign: "center", | |||||
| }, | }, | ||||
| // { | // { | ||||
| // name: "status", | // name: "status", | ||||
| @@ -131,8 +184,39 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| // } | // } | ||||
| // }, | // }, | ||||
| ], | ], | ||||
| [t], | |||||
| [t, onDetailClick, downloadQrCode, handleStockTransfer], | |||||
| ); | ); | ||||
| const handleCloseStockTransferModal = useCallback(() => { | |||||
| setStockTransferModalOpen(false); | |||||
| setSelectedLotLine(null); | |||||
| setStartLocation(""); | |||||
| setTargetLocation(""); | |||||
| setTargetLocationInput(""); | |||||
| setQtyToBeTransferred(0); | |||||
| }, []); | |||||
| const handleSubmitStockTransfer = useCallback(async () => { | |||||
| try { | |||||
| setIsUploading(true); | |||||
| // Decrease the inQty (availableQty) in the source inventory lot line | |||||
| // TODO: Add logic to increase qty in target location warehouse | |||||
| alert(t("Stock transfer successful")); | |||||
| handleCloseStockTransferModal(); | |||||
| // TODO: Refresh the inventory lot lines list | |||||
| } catch (error: any) { | |||||
| console.error("Error transferring stock:", error); | |||||
| alert(error?.message || t("Failed to transfer stock. Please try again.")); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| } | |||||
| }, [selectedLotLine, targetLocation, qtyToBeTransferred, originalQty, handleCloseStockTransferModal, setIsUploading, t]); | |||||
| return <> | return <> | ||||
| <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> | <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> | ||||
| <SearchResults<InventoryLotLineResult> | <SearchResults<InventoryLotLineResult> | ||||
| @@ -142,6 +226,191 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||||
| setPagingController={setPagingController} | setPagingController={setPagingController} | ||||
| totalCount={totalCount} | totalCount={totalCount} | ||||
| /> | /> | ||||
| <Modal | |||||
| open={stockTransferModalOpen} | |||||
| onClose={handleCloseStockTransferModal} | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| alignItems: 'center', | |||||
| justifyContent: 'center', | |||||
| }} | |||||
| > | |||||
| <Card | |||||
| sx={{ | |||||
| position: 'relative', | |||||
| width: '95%', | |||||
| maxWidth: '1200px', | |||||
| maxHeight: '90vh', | |||||
| overflow: 'auto', | |||||
| p: 3, | |||||
| }} | |||||
| > | |||||
| <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> | |||||
| <Typography variant="h6"> | |||||
| {inventory && selectedLotLine | |||||
| ? `${inventory.itemCode} ${inventory.itemName} (${selectedLotLine.lotNo})` | |||||
| : t("Stock Transfer") | |||||
| } | |||||
| </Typography> | |||||
| <IconButton onClick={handleCloseStockTransferModal}> | |||||
| <CloseIcon /> | |||||
| </IconButton> | |||||
| </Box> | |||||
| <Grid container spacing={1} sx={{ mt: 2 }}> | |||||
| <Grid item xs={5.5}> | |||||
| <TextField | |||||
| label={t("Start Location")} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| value={startLocation} | |||||
| disabled | |||||
| InputLabelProps={{ | |||||
| shrink: !!startLocation, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |||||
| <Typography variant="body1">{t("to")}</Typography> | |||||
| </Grid> | |||||
| <Grid item xs={5.5}> | |||||
| <Autocomplete | |||||
| options={warehouses.filter(w => w.code !== startLocation)} | |||||
| getOptionLabel={(option) => option.code || ""} | |||||
| value={targetLocation ? warehouses.find(w => w.code === targetLocation) || null : null} | |||||
| inputValue={targetLocationInput} | |||||
| onInputChange={(event, newInputValue) => { | |||||
| setTargetLocationInput(newInputValue); | |||||
| if (targetLocation && newInputValue !== targetLocation) { | |||||
| setTargetLocation(""); | |||||
| } | |||||
| }} | |||||
| onChange={(event, newValue) => { | |||||
| if (newValue) { | |||||
| setTargetLocation(newValue.code); | |||||
| setTargetLocationInput(newValue.code); | |||||
| } else { | |||||
| setTargetLocation(""); | |||||
| setTargetLocationInput(""); | |||||
| } | |||||
| }} | |||||
| filterOptions={(options, { inputValue }) => { | |||||
| if (!inputValue || inputValue.trim() === "") return options; | |||||
| const searchTerm = inputValue.toLowerCase().trim(); | |||||
| return options.filter((option) => | |||||
| (option.code || "").toLowerCase().includes(searchTerm) || | |||||
| (option.name || "").toLowerCase().includes(searchTerm) || | |||||
| (option.description || "").toLowerCase().includes(searchTerm) | |||||
| ); | |||||
| }} | |||||
| isOptionEqualToValue={(option, value) => option.code === value.code} | |||||
| autoHighlight={false} | |||||
| autoSelect={false} | |||||
| clearOnBlur={false} | |||||
| renderOption={(props, option) => ( | |||||
| <li {...props}> | |||||
| {option.code} | |||||
| </li> | |||||
| )} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t("Target Location")} | |||||
| variant="outlined" | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: !!targetLocation || !!targetLocationInput, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Grid container spacing={1} sx={{ mt: 2 }}> | |||||
| <Grid item xs={2}> | |||||
| <TextField | |||||
| label={t("Original Qty")} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| value={originalQty} | |||||
| disabled | |||||
| InputLabelProps={{ | |||||
| shrink: true, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |||||
| <Typography variant="body1">-</Typography> | |||||
| </Grid> | |||||
| <Grid item xs={2}> | |||||
| <TextField | |||||
| label={t("Qty To Be Transferred")} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| type="number" | |||||
| value={qtyToBeTransferred} | |||||
| onChange={(e) => { | |||||
| const value = parseInt(e.target.value) || 0; | |||||
| const maxValue = Math.max(0, originalQty); | |||||
| setQtyToBeTransferred(Math.min(Math.max(0, value), maxValue)); | |||||
| }} | |||||
| inputProps={{ min: 0, max: originalQty, step: 1 }} | |||||
| InputLabelProps={{ | |||||
| shrink: true, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |||||
| <Typography variant="body1">=</Typography> | |||||
| </Grid> | |||||
| <Grid item xs={2}> | |||||
| <TextField | |||||
| label={t("Remaining Qty")} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| value={remainingQty} | |||||
| disabled | |||||
| InputLabelProps={{ | |||||
| shrink: true, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={2}> | |||||
| <TextField | |||||
| label={t("Stock UoM")} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| value={selectedLotLine?.uom || ""} | |||||
| disabled | |||||
| InputLabelProps={{ | |||||
| shrink: true, | |||||
| sx: { fontSize: "0.9375rem" }, | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={2} sx={{ display: 'flex', alignItems: 'center' }}> | |||||
| <Button | |||||
| variant="contained" | |||||
| fullWidth | |||||
| sx={{ | |||||
| height: '56px', | |||||
| fontSize: '0.9375rem', | |||||
| }} | |||||
| onClick={handleSubmitStockTransfer} | |||||
| disabled={!selectedLotLine || !targetLocation || qtyToBeTransferred <= 0 || qtyToBeTransferred > originalQty} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Card> | |||||
| </Modal> | |||||
| </> | </> | ||||
| } | } | ||||