|
|
@@ -1,10 +1,13 @@ |
|
|
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; |
|
|
import { InventoryLotLineResult, InventoryResult } from "@/app/api/inventory"; |
|
|
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
|
|
|
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
|
|
|
|
|
import SaveIcon from "@mui/icons-material/Save"; |
|
|
|
|
|
import EditIcon from "@mui/icons-material/Edit"; |
|
|
|
|
|
import RestartAltIcon from "@mui/icons-material/RestartAlt"; |
|
|
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 { arrayToDateString } from "@/app/utils/formatUtil"; |
|
|
import { arrayToDateString } from "@/app/utils/formatUtil"; |
|
|
import { Box, Card, Grid, IconButton, Modal, TextField, Typography, Button } from "@mui/material"; |
|
|
|
|
|
|
|
|
import { Box, Card, Checkbox, FormControlLabel, Grid, IconButton, Modal, TextField, Typography, Button, Chip } 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"; |
|
|
@@ -17,6 +20,30 @@ import { WarehouseResult } from "@/app/api/warehouse"; |
|
|
import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; |
|
|
import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; |
|
|
import { createStockTransfer } from "@/app/api/inventory/actions"; |
|
|
import { createStockTransfer } from "@/app/api/inventory/actions"; |
|
|
import { msg, msgError } from "@/components/Swal/CustomAlerts"; |
|
|
import { msg, msgError } from "@/components/Swal/CustomAlerts"; |
|
|
|
|
|
import { PrinterCombo } from "@/app/api/settings/printer"; |
|
|
|
|
|
import { printLabelForInventoryLotLine } from "@/app/api/pdf/actions"; |
|
|
|
|
|
import TuneIcon from "@mui/icons-material/Tune"; |
|
|
|
|
|
import AddIcon from "@mui/icons-material/Add"; |
|
|
|
|
|
import { Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material"; |
|
|
|
|
|
import DeleteIcon from "@mui/icons-material/Delete"; |
|
|
|
|
|
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; |
|
|
|
|
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; |
|
|
|
|
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; |
|
|
|
|
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |
|
|
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
import CheckIcon from "@mui/icons-material/Check"; |
|
|
|
|
|
import { submitStockAdjustment, StockAdjustmentLineRequest } from "@/app/api/stockAdjustment/actions"; |
|
|
|
|
|
|
|
|
|
|
|
type AdjustmentEntry = InventoryLotLineResult & { |
|
|
|
|
|
adjustedQty: number; |
|
|
|
|
|
originalQty?: number; |
|
|
|
|
|
productlotNo?: string; |
|
|
|
|
|
dnNo?: string; |
|
|
|
|
|
isNew?: boolean; |
|
|
|
|
|
isOpeningInventory?: boolean; |
|
|
|
|
|
remarks?: string; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
inventoryLotLines: InventoryLotLineResult[] | null; |
|
|
inventoryLotLines: InventoryLotLineResult[] | null; |
|
|
@@ -25,10 +52,17 @@ interface Props { |
|
|
totalCount: number; |
|
|
totalCount: number; |
|
|
inventory: InventoryResult | null; |
|
|
inventory: InventoryResult | null; |
|
|
onStockTransferSuccess?: () => void | Promise<void>; |
|
|
onStockTransferSuccess?: () => void | Promise<void>; |
|
|
|
|
|
printerCombo?: PrinterCombo[]; |
|
|
|
|
|
onStockAdjustmentSuccess?: () => void | Promise<void>; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory, onStockTransferSuccess }) => { |
|
|
|
|
|
|
|
|
const InventoryLotLineTable: React.FC<Props> = ({ |
|
|
|
|
|
inventoryLotLines, pagingController, setPagingController, totalCount, inventory, |
|
|
|
|
|
onStockTransferSuccess, printerCombo = [], |
|
|
|
|
|
onStockAdjustmentSuccess, |
|
|
|
|
|
}) => { |
|
|
const { t } = useTranslation(["inventory"]); |
|
|
const { t } = useTranslation(["inventory"]); |
|
|
|
|
|
const PRINT_PRINTER_ID_KEY = 'inventoryLotLinePrintPrinterId'; |
|
|
const { setIsUploading } = useUploadContext(); |
|
|
const { setIsUploading } = useUploadContext(); |
|
|
const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); |
|
|
const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); |
|
|
const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null); |
|
|
const [selectedLotLine, setSelectedLotLine] = useState<InventoryLotLineResult | null>(null); |
|
|
@@ -37,7 +71,27 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
const [targetLocationInput, setTargetLocationInput] = useState<string>(""); |
|
|
const [targetLocationInput, setTargetLocationInput] = useState<string>(""); |
|
|
const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0); |
|
|
const [qtyToBeTransferred, setQtyToBeTransferred] = useState<number>(0); |
|
|
const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]); |
|
|
const [warehouses, setWarehouses] = useState<WarehouseResult[]>([]); |
|
|
|
|
|
|
|
|
|
|
|
const [printModalOpen, setPrintModalOpen] = useState(false); |
|
|
|
|
|
const [lotLineForPrint, setLotLineForPrint] = useState<InventoryLotLineResult | null>(null); |
|
|
|
|
|
const [printPrinter, setPrintPrinter] = useState<PrinterCombo | null>(null); |
|
|
|
|
|
const [printQty, setPrintQty] = useState(1); |
|
|
|
|
|
const [stockAdjustmentModalOpen, setStockAdjustmentModalOpen] = useState(false); |
|
|
|
|
|
const [pendingRemovalLineId, setPendingRemovalLineId] = useState<number | null>(null); |
|
|
|
|
|
const [removalReasons, setRemovalReasons] = useState<Record<number, string>>({}); |
|
|
|
|
|
const [addEntryModalOpen, setAddEntryModalOpen] = useState(false); |
|
|
|
|
|
const [addEntryForm, setAddEntryForm] = useState({ |
|
|
|
|
|
lotNo: '', |
|
|
|
|
|
qty: 0, |
|
|
|
|
|
expiryDate: '', |
|
|
|
|
|
locationId: null as number | null, |
|
|
|
|
|
locationInput: '', |
|
|
|
|
|
productlotNo: '', |
|
|
|
|
|
dnNo: '', |
|
|
|
|
|
isOpeningInventory: false, |
|
|
|
|
|
remarks: '', |
|
|
|
|
|
}); |
|
|
|
|
|
const originalAdjustmentLinesRef = useRef<AdjustmentEntry[]>([]); |
|
|
|
|
|
const [adjustmentEntries, setAdjustmentEntries] = useState<AdjustmentEntry[]>([]); |
|
|
useEffect(() => { |
|
|
useEffect(() => { |
|
|
if (stockTransferModalOpen) { |
|
|
if (stockTransferModalOpen) { |
|
|
fetchWarehouseListClient() |
|
|
fetchWarehouseListClient() |
|
|
@@ -46,6 +100,14 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
} |
|
|
} |
|
|
}, [stockTransferModalOpen]); |
|
|
}, [stockTransferModalOpen]); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (addEntryModalOpen) { |
|
|
|
|
|
fetchWarehouseListClient() |
|
|
|
|
|
.then(setWarehouses) |
|
|
|
|
|
.catch(console.error); |
|
|
|
|
|
} |
|
|
|
|
|
}, [addEntryModalOpen]); |
|
|
|
|
|
|
|
|
const availableLotLines = useMemo( |
|
|
const availableLotLines = useMemo( |
|
|
() => (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available"), |
|
|
() => (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available"), |
|
|
[inventoryLotLines] |
|
|
[inventoryLotLines] |
|
|
@@ -53,6 +115,182 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
const originalQty = selectedLotLine?.availableQty || 0; |
|
|
const originalQty = selectedLotLine?.availableQty || 0; |
|
|
const remainingQty = originalQty - qtyToBeTransferred; |
|
|
const remainingQty = originalQty - qtyToBeTransferred; |
|
|
|
|
|
|
|
|
|
|
|
const prevAdjustmentModalOpenRef = useRef(false); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const wasOpen = prevAdjustmentModalOpenRef.current; |
|
|
|
|
|
prevAdjustmentModalOpenRef.current = stockAdjustmentModalOpen; |
|
|
|
|
|
|
|
|
|
|
|
if (stockAdjustmentModalOpen && inventory) { |
|
|
|
|
|
// Only init when we transition to open (modal just opened) |
|
|
|
|
|
if (!wasOpen) { |
|
|
|
|
|
const initial = (availableLotLines ?? []).map((line) => ({ |
|
|
|
|
|
...line, |
|
|
|
|
|
adjustedQty: line.availableQty ?? 0, |
|
|
|
|
|
originalQty: line.availableQty ?? 0, |
|
|
|
|
|
remarks: '', |
|
|
|
|
|
})); |
|
|
|
|
|
setAdjustmentEntries(initial); |
|
|
|
|
|
originalAdjustmentLinesRef.current = initial; |
|
|
|
|
|
} |
|
|
|
|
|
setPendingRemovalLineId(null); |
|
|
|
|
|
setRemovalReasons({}); |
|
|
|
|
|
} |
|
|
|
|
|
}, [stockAdjustmentModalOpen, inventory, availableLotLines]); |
|
|
|
|
|
|
|
|
|
|
|
const handleAdjustmentReset = useCallback(() => { |
|
|
|
|
|
|
|
|
|
|
|
setPendingRemovalLineId(null); |
|
|
|
|
|
setRemovalReasons({}); |
|
|
|
|
|
setAdjustmentEntries( |
|
|
|
|
|
(availableLotLines ?? []).map((line) => ({ |
|
|
|
|
|
...line, |
|
|
|
|
|
adjustedQty: line.availableQty ?? 0, |
|
|
|
|
|
originalQty: line.availableQty ?? 0, |
|
|
|
|
|
remarks: '', |
|
|
|
|
|
})) |
|
|
|
|
|
); |
|
|
|
|
|
}, [availableLotLines]); |
|
|
|
|
|
|
|
|
|
|
|
const handleAdjustmentQtyChange = useCallback((lineId: number, value: number) => { |
|
|
|
|
|
setAdjustmentEntries((prev) => |
|
|
|
|
|
prev.map((line) => |
|
|
|
|
|
line.id === lineId ? { ...line, adjustedQty: Math.max(0, value) } : line |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleAdjustmentRemarksChange = useCallback((lineId: number, value: string) => { |
|
|
|
|
|
setAdjustmentEntries((prev) => |
|
|
|
|
|
prev.map((line) => |
|
|
|
|
|
line.id === lineId ? { ...line, remarks: value } : line |
|
|
|
|
|
) |
|
|
|
|
|
); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleRemoveAdjustmentLine = useCallback((lineId: number) => { |
|
|
|
|
|
setAdjustmentEntries((prev) => prev.filter((line) => line.id !== lineId)); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleRemoveClick = useCallback((lineId: number) => { |
|
|
|
|
|
setPendingRemovalLineId((prev) => (prev === lineId ? null : lineId)); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleRemovalReasonChange = useCallback((lineId: number, value: string) => { |
|
|
|
|
|
setRemovalReasons((prev) => ({ ...prev, [lineId]: value })); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleConfirmRemoval = useCallback((lineId: number) => { |
|
|
|
|
|
setAdjustmentEntries((prev) => prev.filter((line) => line.id !== lineId)); |
|
|
|
|
|
setPendingRemovalLineId(null); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleCancelRemoval = useCallback(() => { |
|
|
|
|
|
setPendingRemovalLineId(null); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const hasAdjustmentChange = useMemo(() => { |
|
|
|
|
|
const original = originalAdjustmentLinesRef.current; |
|
|
|
|
|
const current = adjustmentEntries; |
|
|
|
|
|
if (original.length !== current.length) return true; |
|
|
|
|
|
const origById = new Map(original.map((line) => [line.id, { adjustedQty: line.adjustedQty ?? 0, remarks: line.remarks ?? '' }])); |
|
|
|
|
|
for (const line of current) { |
|
|
|
|
|
const o = origById.get(line.id); |
|
|
|
|
|
if (!o) return true; |
|
|
|
|
|
if (o.adjustedQty !== (line.adjustedQty ?? 0) || (o.remarks ?? '') !== (line.remarks ?? '')) return true; |
|
|
|
|
|
} |
|
|
|
|
|
return false; |
|
|
|
|
|
}, [adjustmentEntries]); |
|
|
|
|
|
|
|
|
|
|
|
const toApiLine = useCallback((line: AdjustmentEntry, itemCode: string): StockAdjustmentLineRequest => { |
|
|
|
|
|
const [y, m, d] = Array.isArray(line.expiryDate) ? line.expiryDate : []; |
|
|
|
|
|
const expiryDate = y != null && m != null && d != null |
|
|
|
|
|
? `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}` |
|
|
|
|
|
: ''; |
|
|
|
|
|
return { |
|
|
|
|
|
id: line.id, |
|
|
|
|
|
lotNo: line.lotNo ?? null, |
|
|
|
|
|
adjustedQty: line.adjustedQty ?? 0, |
|
|
|
|
|
productlotNo: line.productlotNo ?? null, |
|
|
|
|
|
dnNo: line.dnNo ?? null, |
|
|
|
|
|
isOpeningInventory: line.isOpeningInventory ?? false, |
|
|
|
|
|
isNew: line.isNew ?? false, |
|
|
|
|
|
itemId: line.item?.id ?? 0, |
|
|
|
|
|
itemNo: line.item?.code ?? itemCode, |
|
|
|
|
|
expiryDate, |
|
|
|
|
|
warehouseId: line.warehouse?.id ?? 0, |
|
|
|
|
|
uom: line.uom ?? null, |
|
|
|
|
|
}; |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleAdjustmentSave = useCallback(async () => { |
|
|
|
|
|
if (!inventory) return; |
|
|
|
|
|
const itemCode = inventory.itemCode; |
|
|
|
|
|
const originalLines = originalAdjustmentLinesRef.current.map((line) => toApiLine(line, itemCode)); |
|
|
|
|
|
const currentLines = adjustmentEntries.map((line) => toApiLine(line, itemCode)); |
|
|
|
|
|
try { |
|
|
|
|
|
setIsUploading(true); |
|
|
|
|
|
await submitStockAdjustment({ |
|
|
|
|
|
itemId: inventory.itemId, |
|
|
|
|
|
originalLines, |
|
|
|
|
|
currentLines, |
|
|
|
|
|
}); |
|
|
|
|
|
msg(t("Saved successfully")); |
|
|
|
|
|
setStockAdjustmentModalOpen(false); |
|
|
|
|
|
await onStockAdjustmentSuccess?.(); |
|
|
|
|
|
} catch (e: unknown) { |
|
|
|
|
|
const message = e instanceof Error ? e.message : String(e); |
|
|
|
|
|
msgError(message || t("Save failed")); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setIsUploading(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, [adjustmentEntries, inventory, t, toApiLine, onStockAdjustmentSuccess]); |
|
|
|
|
|
|
|
|
|
|
|
const handleOpenAddEntry = useCallback(() => { |
|
|
|
|
|
setAddEntryForm({ |
|
|
|
|
|
lotNo: '', |
|
|
|
|
|
qty: 0, |
|
|
|
|
|
expiryDate: '', |
|
|
|
|
|
locationId: null, |
|
|
|
|
|
locationInput: '', |
|
|
|
|
|
productlotNo: '', |
|
|
|
|
|
dnNo: '', |
|
|
|
|
|
isOpeningInventory: false, |
|
|
|
|
|
remarks: '', |
|
|
|
|
|
}); |
|
|
|
|
|
setAddEntryModalOpen(true); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
const handleAddEntrySubmit = useCallback(() => { |
|
|
|
|
|
if (addEntryForm.qty < 0 || !addEntryForm.expiryDate || !addEntryForm.locationId || !inventory) return; |
|
|
|
|
|
const warehouse = warehouses.find(w => w.id === addEntryForm.locationId); |
|
|
|
|
|
if (!warehouse) return; |
|
|
|
|
|
const [y, m, d] = addEntryForm.expiryDate.split('-').map(Number); |
|
|
|
|
|
const newEntry: AdjustmentEntry = { |
|
|
|
|
|
id: -Date.now(), |
|
|
|
|
|
lotNo: addEntryForm.lotNo.trim() || '', |
|
|
|
|
|
item: { id: inventory.itemId, code: inventory.itemCode, name: inventory.itemName, type: inventory.itemType }, |
|
|
|
|
|
warehouse: { id: warehouse.id, code: warehouse.code, name: warehouse.name }, |
|
|
|
|
|
inQty: 0, outQty: 0, holdQty: 0, |
|
|
|
|
|
expiryDate: [y, m, d], |
|
|
|
|
|
status: 'available', |
|
|
|
|
|
availableQty: addEntryForm.qty, |
|
|
|
|
|
uom: inventory.uomUdfudesc || inventory.uomShortDesc || inventory.uomCode, |
|
|
|
|
|
qtyPerSmallestUnit: inventory.qtyPerSmallestUnit ?? 1, |
|
|
|
|
|
baseUom: inventory.baseUom || '', |
|
|
|
|
|
stockInLineId: 0, |
|
|
|
|
|
originalQty: 0, |
|
|
|
|
|
adjustedQty: addEntryForm.qty, |
|
|
|
|
|
productlotNo: addEntryForm.productlotNo.trim() || undefined, |
|
|
|
|
|
dnNo: addEntryForm.dnNo.trim() || undefined, |
|
|
|
|
|
isNew: true, |
|
|
|
|
|
isOpeningInventory: addEntryForm.isOpeningInventory, |
|
|
|
|
|
remarks: addEntryForm.remarks?.trim() ?? '', |
|
|
|
|
|
}; |
|
|
|
|
|
setAdjustmentEntries(prev => [...prev, newEntry]); |
|
|
|
|
|
setAddEntryModalOpen(false); |
|
|
|
|
|
}, [addEntryForm, inventory, warehouses]); |
|
|
|
|
|
|
|
|
const downloadQrCode = useCallback(async (lotLineId: number) => { |
|
|
const downloadQrCode = useCallback(async (lotLineId: number) => { |
|
|
setIsUploading(true); |
|
|
setIsUploading(true); |
|
|
// const postData = { stockInLineIds: [42,43,44] }; |
|
|
// const postData = { stockInLineIds: [42,43,44] }; |
|
|
@@ -78,6 +316,34 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
[], |
|
|
[], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const handlePrintClick = useCallback((lotLine: InventoryLotLineResult) => { |
|
|
|
|
|
setLotLineForPrint(lotLine); |
|
|
|
|
|
const labelPrinters = (printerCombo || []).filter(p => p.type === 'Label'); |
|
|
|
|
|
const savedId = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(PRINT_PRINTER_ID_KEY) : null; |
|
|
|
|
|
const savedPrinter = savedId ? labelPrinters.find(p => p.id === Number(savedId)) : null; |
|
|
|
|
|
setPrintPrinter(savedPrinter ?? labelPrinters[0] ?? null); |
|
|
|
|
|
setPrintQty(1); |
|
|
|
|
|
setPrintModalOpen(true); |
|
|
|
|
|
}, [printerCombo]); |
|
|
|
|
|
|
|
|
|
|
|
const handlePrintConfirm = useCallback(async () => { |
|
|
|
|
|
if (!lotLineForPrint || !printPrinter) return; |
|
|
|
|
|
try { |
|
|
|
|
|
setIsUploading(true); |
|
|
|
|
|
await printLabelForInventoryLotLine({ |
|
|
|
|
|
inventoryLotLineId: lotLineForPrint.id, |
|
|
|
|
|
printerId: printPrinter.id, |
|
|
|
|
|
printQty, |
|
|
|
|
|
}); |
|
|
|
|
|
msg(t("Print sent")); |
|
|
|
|
|
setPrintModalOpen(false); |
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
msgError(e?.message ?? t("Print failed")); |
|
|
|
|
|
} finally { |
|
|
|
|
|
setIsUploading(false); |
|
|
|
|
|
} |
|
|
|
|
|
}, [lotLineForPrint, printPrinter, printQty, setIsUploading, t]); |
|
|
|
|
|
|
|
|
const onDetailClick = useCallback( |
|
|
const onDetailClick = useCallback( |
|
|
(lotLine: InventoryLotLineResult) => { |
|
|
(lotLine: InventoryLotLineResult) => { |
|
|
downloadQrCode(lotLine.id) |
|
|
downloadQrCode(lotLine.id) |
|
|
@@ -163,7 +429,7 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
{ |
|
|
{ |
|
|
name: "id", |
|
|
name: "id", |
|
|
label: t("Print QR Code"), |
|
|
label: t("Print QR Code"), |
|
|
onClick: () => {}, |
|
|
|
|
|
|
|
|
onClick: handlePrintClick, |
|
|
buttonIcon: <PrintIcon />, |
|
|
buttonIcon: <PrintIcon />, |
|
|
align: "center", |
|
|
align: "center", |
|
|
headerAlign: "center", |
|
|
headerAlign: "center", |
|
|
@@ -190,9 +456,11 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
// } |
|
|
// } |
|
|
// }, |
|
|
// }, |
|
|
], |
|
|
], |
|
|
[t, onDetailClick, downloadQrCode, handleStockTransfer], |
|
|
|
|
|
|
|
|
[t, onDetailClick, downloadQrCode, handleStockTransfer, handlePrintClick], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleCloseStockTransferModal = useCallback(() => { |
|
|
const handleCloseStockTransferModal = useCallback(() => { |
|
|
setStockTransferModalOpen(false); |
|
|
setStockTransferModalOpen(false); |
|
|
setSelectedLotLine(null); |
|
|
setSelectedLotLine(null); |
|
|
@@ -234,7 +502,31 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
}, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t, onStockTransferSuccess]); |
|
|
}, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t, onStockTransferSuccess]); |
|
|
|
|
|
|
|
|
return <> |
|
|
return <> |
|
|
<Typography variant="h6">{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")}</Typography> |
|
|
|
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', mb: 2 }}> |
|
|
|
|
|
<Typography variant="h6"> |
|
|
|
|
|
{inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
{inventory && ( |
|
|
|
|
|
<Chip |
|
|
|
|
|
icon={<TuneIcon />} |
|
|
|
|
|
label={t("Stock Adjustment")} |
|
|
|
|
|
onClick={() => setStockAdjustmentModalOpen(true)} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
cursor: 'pointer', |
|
|
|
|
|
height: 30, |
|
|
|
|
|
fontWeight: 'bold', |
|
|
|
|
|
'& .MuiChip-label': { |
|
|
|
|
|
fontSize: '0.875rem', |
|
|
|
|
|
fontWeight: 'bold', |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiChip-icon': { |
|
|
|
|
|
fontSize: '1rem', |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
)} |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
<SearchResults<InventoryLotLineResult> |
|
|
<SearchResults<InventoryLotLineResult> |
|
|
items={availableLotLines} |
|
|
items={availableLotLines} |
|
|
columns={columns} |
|
|
columns={columns} |
|
|
@@ -428,7 +720,343 @@ const InventoryLotLineTable: React.FC<Props> = ({ inventoryLotLines, pagingContr |
|
|
</Card> |
|
|
</Card> |
|
|
</Modal> |
|
|
</Modal> |
|
|
|
|
|
|
|
|
</> |
|
|
|
|
|
|
|
|
<Modal |
|
|
|
|
|
open={printModalOpen} |
|
|
|
|
|
onClose={() => setPrintModalOpen(false)} |
|
|
|
|
|
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} |
|
|
|
|
|
> |
|
|
|
|
|
<Card sx={{ position: 'relative', minWidth: 320, maxWidth: 480, p: 3 }}> |
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> |
|
|
|
|
|
<Typography variant="h6">{t("Print QR Code")}</Typography> |
|
|
|
|
|
<IconButton onClick={() => setPrintModalOpen(false)}><CloseIcon /></IconButton> |
|
|
|
|
|
</Box> |
|
|
|
|
|
<Grid container spacing={2}> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Autocomplete |
|
|
|
|
|
options={(printerCombo || []).filter(printer => printer.type === 'Label')} |
|
|
|
|
|
getOptionLabel={(opt) => opt.name ?? opt.label ?? opt.code ?? `Printer ${opt.id}`} |
|
|
|
|
|
value={printPrinter} |
|
|
|
|
|
onChange={(_, v) => { |
|
|
|
|
|
setPrintPrinter(v); |
|
|
|
|
|
if (typeof sessionStorage !== 'undefined') { |
|
|
|
|
|
if (v?.id != null) sessionStorage.setItem(PRINT_PRINTER_ID_KEY, String(v.id)); |
|
|
|
|
|
else sessionStorage.removeItem(PRINT_PRINTER_ID_KEY); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
renderInput={(params) => <TextField {...params} label={t("Printer")} />} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("Print Qty")} |
|
|
|
|
|
type="number" |
|
|
|
|
|
value={printQty} |
|
|
|
|
|
onChange={(e) => setPrintQty(Math.max(1, parseInt(e.target.value) || 1))} |
|
|
|
|
|
inputProps={{ min: 1 }} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Button variant="contained" fullWidth onClick={handlePrintConfirm} disabled={!printPrinter}> |
|
|
|
|
|
{t("Print")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Modal> |
|
|
|
|
|
<Modal |
|
|
|
|
|
open={stockAdjustmentModalOpen} |
|
|
|
|
|
onClose={() => setStockAdjustmentModalOpen(false)} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
display: 'flex', |
|
|
|
|
|
alignItems: 'center', |
|
|
|
|
|
justifyContent: 'center', |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Card |
|
|
|
|
|
sx={{ |
|
|
|
|
|
position: 'relative', |
|
|
|
|
|
width: '95%', |
|
|
|
|
|
maxWidth: '1400px', |
|
|
|
|
|
maxHeight: '92vh', |
|
|
|
|
|
overflow: 'auto', |
|
|
|
|
|
p: 3, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> |
|
|
|
|
|
<Typography variant="h6"> |
|
|
|
|
|
{inventory |
|
|
|
|
|
? `${t("Edit mode")}: ${inventory.itemCode} ${inventory.itemName}` |
|
|
|
|
|
: t("Stock Adjustment") |
|
|
|
|
|
} |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<IconButton onClick={() => setStockAdjustmentModalOpen(false)}> |
|
|
|
|
|
<CloseIcon /> |
|
|
|
|
|
</IconButton> |
|
|
|
|
|
</Box> |
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
startIcon={<AddIcon />} |
|
|
|
|
|
onClick={handleOpenAddEntry} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Add entry")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="outlined" |
|
|
|
|
|
startIcon={<RestartAltIcon />} |
|
|
|
|
|
onClick={handleAdjustmentReset} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Reset")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
color="primary" |
|
|
|
|
|
startIcon={<SaveIcon />} |
|
|
|
|
|
onClick={handleAdjustmentSave} |
|
|
|
|
|
disabled={!hasAdjustmentChange} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Save")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
|
|
|
|
|
|
{/* List view */} |
|
|
|
|
|
<Box sx={{ overflow: 'auto' }}> |
|
|
|
|
|
<Table size="small"> |
|
|
|
|
|
<TableHead> |
|
|
|
|
|
<TableRow> |
|
|
|
|
|
<TableCell>{t("Lot No")}</TableCell> |
|
|
|
|
|
<TableCell align="right">{t("Original Qty")}</TableCell> |
|
|
|
|
|
<TableCell align="right">{t("Adjusted Qty")}</TableCell> |
|
|
|
|
|
<TableCell align="right" sx={{ minWidth: 100 }}>{t("Difference")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Stock UoM")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Expiry Date")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Location")}</TableCell> |
|
|
|
|
|
<TableCell>{t("Remarks")}</TableCell> |
|
|
|
|
|
<TableCell align="center" sx={{ minWidth: 240 }}>{t("Action")}</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
</TableHead> |
|
|
|
|
|
<TableBody> |
|
|
|
|
|
{adjustmentEntries.map((line) => ( |
|
|
|
|
|
<TableRow |
|
|
|
|
|
key={line.id} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
backgroundColor: pendingRemovalLineId === line.id ? 'action.hover' : undefined, |
|
|
|
|
|
}} |
|
|
|
|
|
> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
<Box component="span" sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}> |
|
|
|
|
|
<span> |
|
|
|
|
|
{line.lotNo?.trim() ? line.lotNo : t("No lot no entered, will be generated by system.")} |
|
|
|
|
|
{line.isOpeningInventory && ` (${t("Opening Inventory")})`} |
|
|
|
|
|
</span> |
|
|
|
|
|
{line.productlotNo && <span>{t("productLotNo")}: {line.productlotNo}</span>} |
|
|
|
|
|
{line.dnNo && <span>{t("dnNo")}: {line.dnNo}</span>} |
|
|
|
|
|
</Box> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell align="right">{line.originalQty ?? 0}</TableCell> |
|
|
|
|
|
<TableCell align="right"> |
|
|
|
|
|
<TextField |
|
|
|
|
|
type="text" |
|
|
|
|
|
inputMode="numeric" |
|
|
|
|
|
value={String(line.adjustedQty)} |
|
|
|
|
|
onChange={(e) => { |
|
|
|
|
|
const raw = e.target.value.replace(/\D/g, ''); |
|
|
|
|
|
if (raw === '') { |
|
|
|
|
|
handleAdjustmentQtyChange(line.id, 0); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const num = parseInt(raw, 10); |
|
|
|
|
|
if (!Number.isNaN(num) && num >= 0) handleAdjustmentQtyChange(line.id, num); |
|
|
|
|
|
}} |
|
|
|
|
|
inputProps={{ style: { textAlign: 'right' } }} |
|
|
|
|
|
size="small" |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 120, |
|
|
|
|
|
'& .MuiInputBase-root': { |
|
|
|
|
|
display: 'flex', |
|
|
|
|
|
alignItems: 'center', |
|
|
|
|
|
height: 56, |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input': { |
|
|
|
|
|
fontSize: 16, |
|
|
|
|
|
textAlign: 'right', |
|
|
|
|
|
height: 40, |
|
|
|
|
|
lineHeight: '40px', |
|
|
|
|
|
paddingTop: 0, |
|
|
|
|
|
paddingBottom: 0, |
|
|
|
|
|
boxSizing: 'border-box', |
|
|
|
|
|
MozAppearance: 'textfield', |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input::-webkit-outer-spin-button': { |
|
|
|
|
|
WebkitAppearance: 'none', |
|
|
|
|
|
margin: 0, |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input::-webkit-inner-spin-button': { |
|
|
|
|
|
WebkitAppearance: 'none', |
|
|
|
|
|
margin: 0, |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell align="right" sx={{ minWidth: 100, fontWeight: 700 }}> |
|
|
|
|
|
{(() => { |
|
|
|
|
|
const diff = line.adjustedQty - (line.originalQty ?? 0); |
|
|
|
|
|
const text = diff > 0 ? `+${diff}` : diff < 0 ? `${diff}` : '±0'; |
|
|
|
|
|
const color = diff > 0 ? 'success.main' : diff < 0 ? 'error.main' : 'text.secondary'; |
|
|
|
|
|
return <Box component="span" sx={{ color }}>{text}</Box>; |
|
|
|
|
|
})()} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell>{line.uom}</TableCell> |
|
|
|
|
|
<TableCell>{arrayToDateString(line.expiryDate)}</TableCell> |
|
|
|
|
|
<TableCell>{line.warehouse?.code ?? ""}</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
{pendingRemovalLineId === line.id ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
placeholder={t("Reason for removal")} |
|
|
|
|
|
value={removalReasons[line.id] ?? ""} |
|
|
|
|
|
onChange={(e) => handleRemovalReasonChange(line.id, e.target.value)} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 160, |
|
|
|
|
|
maxWidth: '100%', |
|
|
|
|
|
'& .MuiInputBase-root': { |
|
|
|
|
|
display: 'flex', |
|
|
|
|
|
alignItems: 'center', |
|
|
|
|
|
height: 56, |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input': { |
|
|
|
|
|
fontSize: '1rem', |
|
|
|
|
|
height: 40, |
|
|
|
|
|
lineHeight: '40px', |
|
|
|
|
|
paddingTop: 0, |
|
|
|
|
|
paddingBottom: 0, |
|
|
|
|
|
boxSizing: 'border-box', |
|
|
|
|
|
'&::placeholder': { color: '#9e9e9e', opacity: 1 }, |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
) : (line.adjustedQty - (line.originalQty ?? 0)) !== 0 ? ( |
|
|
|
|
|
<TextField |
|
|
|
|
|
size="small" |
|
|
|
|
|
placeholder={t("Reason for adjustment")} |
|
|
|
|
|
value={line.remarks ?? ""} |
|
|
|
|
|
onChange={(e) => handleAdjustmentRemarksChange(line.id, e.target.value)} |
|
|
|
|
|
sx={{ |
|
|
|
|
|
width: 160, |
|
|
|
|
|
maxWidth: '100%', |
|
|
|
|
|
'& .MuiInputBase-root': { |
|
|
|
|
|
display: 'flex', |
|
|
|
|
|
alignItems: 'center', |
|
|
|
|
|
height: 56, |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiInputBase-input': { |
|
|
|
|
|
fontSize: '1rem', |
|
|
|
|
|
height: 40, |
|
|
|
|
|
lineHeight: '40px', |
|
|
|
|
|
paddingTop: 0, |
|
|
|
|
|
paddingBottom: 0, |
|
|
|
|
|
boxSizing: 'border-box', |
|
|
|
|
|
'&::placeholder': { color: '#9e9e9e', opacity: 1 }, |
|
|
|
|
|
}, |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
) : null} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
<TableCell align="center"> |
|
|
|
|
|
{pendingRemovalLineId === line.id ? ( |
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5 }}> |
|
|
|
|
|
<Button size="small" variant="outlined" onClick={handleCancelRemoval}> |
|
|
|
|
|
{t("Cancel")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
size="small" |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
color="error" |
|
|
|
|
|
startIcon={<CheckIcon />} |
|
|
|
|
|
onClick={() => handleConfirmRemoval(line.id)} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Confirm remove")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Box> |
|
|
|
|
|
) : ( |
|
|
|
|
|
<IconButton |
|
|
|
|
|
size="small" |
|
|
|
|
|
onClick={() => handleRemoveClick(line.id)} |
|
|
|
|
|
color="error" |
|
|
|
|
|
title={t("Remove")} |
|
|
|
|
|
> |
|
|
|
|
|
<DeleteIcon fontSize="small" /> |
|
|
|
|
|
</IconButton> |
|
|
|
|
|
)} |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
))} |
|
|
|
|
|
</TableBody> |
|
|
|
|
|
</Table> |
|
|
|
|
|
</Box> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Modal> |
|
|
|
|
|
|
|
|
|
|
|
<Modal |
|
|
|
|
|
open={addEntryModalOpen} |
|
|
|
|
|
onClose={() => setAddEntryModalOpen(false)} |
|
|
|
|
|
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} |
|
|
|
|
|
> |
|
|
|
|
|
<Card sx={{ position: 'relative', minWidth: 600, maxWidth: 900, p: 3 }}> |
|
|
|
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> |
|
|
|
|
|
<Typography variant="h6">{t("Add entry")}</Typography> |
|
|
|
|
|
<IconButton onClick={() => setAddEntryModalOpen(false)}><CloseIcon /></IconButton> |
|
|
|
|
|
</Box> |
|
|
|
|
|
<Grid container spacing={2}> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<TextField label={t("Available Qty")} type="number" fullWidth required value={addEntryForm.qty || ''} onChange={(e) => setAddEntryForm(f => ({ ...f, qty: Math.max(0, parseInt(e.target.value) || 0) }))} inputProps={{ min: 0 }} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<TextField label={t("Stock UoM")} fullWidth disabled value={inventory?.uomUdfudesc || inventory?.uomShortDesc || inventory?.uomCode || ''} sx={{ '& .MuiInputBase-input': { color: 'text.secondary' } }} InputLabelProps={{ shrink: true }} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}> |
|
|
|
|
|
<DatePicker label={t("Expiry Date")} format={INPUT_DATE_FORMAT} value={addEntryForm.expiryDate ? dayjs(addEntryForm.expiryDate) : null} onChange={(value) => setAddEntryForm(f => ({ ...f, expiryDate: value ? dayjs(value).format(INPUT_DATE_FORMAT) : '' }))} slotProps={{ textField: { fullWidth: true, required: true } }} /> |
|
|
|
|
|
</LocalizationProvider> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={6}> |
|
|
|
|
|
<Autocomplete options={warehouses} getOptionLabel={(o) => o.code || ''} value={addEntryForm.locationId ? warehouses.find(w => w.id === addEntryForm.locationId) ?? null : null} inputValue={addEntryForm.locationInput} onInputChange={(_, v) => setAddEntryForm(f => ({ ...f, locationInput: v }))} onChange={(_, v) => setAddEntryForm(f => ({ ...f, locationId: v?.id ?? null, locationInput: v?.code ?? '' }))} renderInput={(params) => <TextField {...params} label={t("Location")} required />} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={6} sx={{ display: 'flex', alignItems: 'center' }}> |
|
|
|
|
|
<FormControlLabel control={<Checkbox checked={addEntryForm.isOpeningInventory} onChange={(e) => setAddEntryForm(f => ({ ...f, isOpeningInventory: e.target.checked }))} />} label={t("Opening Inventory")} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<TextField label={t("productLotNo")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.productlotNo} onChange={(e) => setAddEntryForm(f => ({ ...f, productlotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<TextField label={t("dnNo")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.dnNo} onChange={(e) => setAddEntryForm(f => ({ ...f, dnNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={4}> |
|
|
|
|
|
<TextField label={t("Lot No")} fullWidth placeholder={t("Optional - system will generate")} value={addEntryForm.lotNo} onChange={(e) => setAddEntryForm(f => ({ ...f, lotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<TextField |
|
|
|
|
|
label={t("Remarks")} |
|
|
|
|
|
fullWidth |
|
|
|
|
|
placeholder={t("Reason for adjustment")} |
|
|
|
|
|
value={addEntryForm.remarks} |
|
|
|
|
|
onChange={(e) => setAddEntryForm(f => ({ ...f, remarks: e.target.value }))} |
|
|
|
|
|
multiline |
|
|
|
|
|
minRows={2} |
|
|
|
|
|
sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} |
|
|
|
|
|
/> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
<Grid item xs={12}> |
|
|
|
|
|
<Button variant="contained" fullWidth onClick={handleAddEntrySubmit} disabled={addEntryForm.qty < 0 || !addEntryForm.expiryDate || !addEntryForm.locationId}> |
|
|
|
|
|
{t("Add")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
</Grid> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Modal> |
|
|
|
|
|
</> |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export default InventoryLotLineTable; |
|
|
export default InventoryLotLineTable; |