| @@ -1,16 +1,21 @@ | |||
| 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 { Column } from "../SearchResults"; | |||
| import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; | |||
| import { CheckCircleOutline, DoDisturb, EditNote } from "@mui/icons-material"; | |||
| 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 { downloadFile } from "@/app/utils/commonUtil"; | |||
| import { fetchQrCodeByLotLineId, LotLineToQrcode } from "@/app/api/pdf/actions"; | |||
| 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 { | |||
| inventoryLotLines: InventoryLotLineResult[] | null; | |||
| @@ -23,8 +28,26 @@ interface Props { | |||
| const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory }) => { | |||
| const { t } = useTranslation(["inventory"]); | |||
| 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); | |||
| // const postData = { stockInLineIds: [42,43,44] }; | |||
| const postData: LotLineToQrcode = { | |||
| @@ -37,12 +60,24 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||
| setIsUploading(false); | |||
| }, [setIsUploading]); | |||
| const handleStockTransfer = useCallback( | |||
| (lotLine: InventoryLotLineResult) => { | |||
| setSelectedLotLine(lotLine); | |||
| setStockTransferModalOpen(true); | |||
| setStartLocation(lotLine.warehouse.code || ""); | |||
| setTargetLocation(""); | |||
| setTargetLocationInput(""); | |||
| setQtyToBeTransferred(0); | |||
| }, | |||
| [], | |||
| ); | |||
| const onDetailClick = useCallback( | |||
| (lotLine: InventoryLotLineResult) => { | |||
| printQrcode(lotLine.id) | |||
| downloadQrCode(lotLine.id) | |||
| // lot line id to find stock in line | |||
| }, | |||
| [printQrcode], | |||
| [downloadQrCode], | |||
| ); | |||
| const columns = useMemo<Column<InventoryLotLineResult>[]>( | |||
| () => [ | |||
| @@ -108,14 +143,32 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||
| name: "warehouse", | |||
| label: t("Warehouse"), | |||
| renderCell: (params) => { | |||
| return `${params.warehouse.code} - ${params.warehouse.name}` | |||
| return `${params.warehouse.code}` | |||
| }, | |||
| }, | |||
| { | |||
| name: "id", | |||
| label: t("qrcode"), | |||
| label: t("Download QR Code"), | |||
| onClick: onDetailClick, | |||
| 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", | |||
| @@ -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 <> | |||
| <Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> | |||
| <SearchResults<InventoryLotLineResult> | |||
| @@ -142,6 +226,191 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr | |||
| setPagingController={setPagingController} | |||
| 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> | |||
| </> | |||
| } | |||