diff --git a/src/app/api/pdf/actions.ts b/src/app/api/pdf/actions.ts index ae34e0d..74a050a 100644 --- a/src/app/api/pdf/actions.ts +++ b/src/app/api/pdf/actions.ts @@ -2,7 +2,7 @@ // import { serverFetchBlob } from "@/app/utils/fetchUtil"; // import { BASE_API_URL } from "@/config/api"; -import { serverFetchBlob } from "../../utils/fetchUtil"; +import { serverFetchBlob, serverFetchWithNoContent } from "../../utils/fetchUtil"; import { BASE_API_URL } from "../../../config/api"; export interface FileResponse { @@ -12,7 +12,7 @@ export interface FileResponse { export const fetchPoQrcode = async (data: any) => { const reportBlob = await serverFetchBlob( - `${BASE_API_URL}/stockInLine/print-label`, + `${BASE_API_URL}/stockInLine/download-label`, { method: "POST", body: JSON.stringify(data), @@ -27,7 +27,7 @@ export interface LotLineToQrcode { } export const fetchQrCodeByLotLineId = async (data: LotLineToQrcode) => { const reportBlob = await serverFetchBlob( - `${BASE_API_URL}/inventoryLotLine/print-label`, + `${BASE_API_URL}/inventoryLotLine/download-label`, { method: "POST", body: JSON.stringify(data), @@ -37,3 +37,22 @@ export const fetchQrCodeByLotLineId = async (data: LotLineToQrcode) => { return reportBlob; } + +export interface PrintLabelForInventoryLotLineRequest { + inventoryLotLineId: number; + printerId: number; + printQty?: number; +} + +export async function printLabelForInventoryLotLine(data: PrintLabelForInventoryLotLineRequest) { + const params = new URLSearchParams(); + params.append("inventoryLotLineId", data.inventoryLotLineId.toString()); + params.append("printerId", data.printerId.toString()); + if (data.printQty != null && data.printQty !== undefined) { + params.append("printQty", data.printQty.toString()); + } + return serverFetchWithNoContent( + `${BASE_API_URL}/inventoryLotLine/print-label?${params.toString()}`, + { method: "GET" } + ); +} diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts index 4bae861..afe937b 100644 --- a/src/app/api/po/actions.ts +++ b/src/app/api/po/actions.ts @@ -250,7 +250,7 @@ export const testing = cache(async (queryParams?: Record) => { // DEPRECIATED export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => { const params = convertObjToURLSearchParams(data) - return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`, + return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/print-label?${params}`, { method: "GET", headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/stockAdjustment/actions.ts b/src/app/api/stockAdjustment/actions.ts new file mode 100644 index 0000000..cbcb04d --- /dev/null +++ b/src/app/api/stockAdjustment/actions.ts @@ -0,0 +1,48 @@ +"use server"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; + +export interface StockAdjustmentLineRequest { + id: number; + lotNo?: string | null; + adjustedQty: number; + productlotNo?: string | null; + dnNo?: string | null; + isOpeningInventory: boolean; + isNew: boolean; + itemId: number; + itemNo: string; + expiryDate: string; + warehouseId: number; + uom?: string | null; +} + +export interface StockAdjustmentRequest { + itemId: number; + originalLines: StockAdjustmentLineRequest[]; + currentLines: StockAdjustmentLineRequest[]; +} + +export interface MessageResponse { + id: number | null; + name: string; + code: string; + type: string; + message: string | null; + errorPosition: string | null; +} + +export const submitStockAdjustment = async (data: StockAdjustmentRequest) => { + const result = await serverFetchJson( + `${BASE_API_URL}/stockAdjustment/submit`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + revalidateTag("inventoryLotLines"); + revalidateTag("inventories"); + return result; +}; \ No newline at end of file diff --git a/src/app/api/stockIn/actions.ts b/src/app/api/stockIn/actions.ts index f6e2dcd..0b152d7 100644 --- a/src/app/api/stockIn/actions.ts +++ b/src/app/api/stockIn/actions.ts @@ -13,6 +13,7 @@ import { Uom } from "../settings/uom"; import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; // import { BASE_API_URL } from "@/config/api"; import { Result } from "../settings/item"; + export interface PostStockInLineResponse { id: number | null; name: string; @@ -232,7 +233,7 @@ export const testing = cache(async (queryParams?: Record) => { export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => { const params = convertObjToURLSearchParams(data) - return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`, + return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/print-label?${params}`, { method: "GET", headers: { "Content-Type": "application/json" }, diff --git a/src/components/InventorySearch/InventoryLotLineTable.tsx b/src/components/InventorySearch/InventoryLotLineTable.tsx index 551f1cf..3df6219 100644 --- a/src/components/InventorySearch/InventoryLotLineTable.tsx +++ b/src/components/InventorySearch/InventoryLotLineTable.tsx @@ -1,10 +1,13 @@ 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 { Column } from "../SearchResults"; import SearchResults, { defaultPagingController, defaultSetPagingController } from "../SearchResults/SearchResults"; 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 { downloadFile } from "@/app/utils/commonUtil"; 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 { createStockTransfer } from "@/app/api/inventory/actions"; 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 { inventoryLotLines: InventoryLotLineResult[] | null; @@ -25,10 +52,17 @@ interface Props { totalCount: number; inventory: InventoryResult | null; onStockTransferSuccess?: () => void | Promise; + printerCombo?: PrinterCombo[]; + onStockAdjustmentSuccess?: () => void | Promise; } -const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingController, setPagingController, totalCount, inventory, onStockTransferSuccess }) => { +const InventoryLotLineTable: React.FC = ({ + inventoryLotLines, pagingController, setPagingController, totalCount, inventory, + onStockTransferSuccess, printerCombo = [], + onStockAdjustmentSuccess, +}) => { const { t } = useTranslation(["inventory"]); + const PRINT_PRINTER_ID_KEY = 'inventoryLotLinePrintPrinterId'; const { setIsUploading } = useUploadContext(); const [stockTransferModalOpen, setStockTransferModalOpen] = useState(false); const [selectedLotLine, setSelectedLotLine] = useState(null); @@ -37,7 +71,27 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr const [targetLocationInput, setTargetLocationInput] = useState(""); const [qtyToBeTransferred, setQtyToBeTransferred] = useState(0); const [warehouses, setWarehouses] = useState([]); - + const [printModalOpen, setPrintModalOpen] = useState(false); + const [lotLineForPrint, setLotLineForPrint] = useState(null); + const [printPrinter, setPrintPrinter] = useState(null); + const [printQty, setPrintQty] = useState(1); + const [stockAdjustmentModalOpen, setStockAdjustmentModalOpen] = useState(false); + const [pendingRemovalLineId, setPendingRemovalLineId] = useState(null); + const [removalReasons, setRemovalReasons] = useState>({}); + 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([]); + const [adjustmentEntries, setAdjustmentEntries] = useState([]); useEffect(() => { if (stockTransferModalOpen) { fetchWarehouseListClient() @@ -46,6 +100,14 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr } }, [stockTransferModalOpen]); + useEffect(() => { + if (addEntryModalOpen) { + fetchWarehouseListClient() + .then(setWarehouses) + .catch(console.error); + } + }, [addEntryModalOpen]); + const availableLotLines = useMemo( () => (inventoryLotLines ?? []).filter((line) => line.status?.toLowerCase() === "available"), [inventoryLotLines] @@ -53,6 +115,182 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr const originalQty = selectedLotLine?.availableQty || 0; 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) => { setIsUploading(true); // const postData = { stockInLineIds: [42,43,44] }; @@ -78,6 +316,34 @@ const InventoryLotLineTable: React.FC = ({ 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( (lotLine: InventoryLotLineResult) => { downloadQrCode(lotLine.id) @@ -163,7 +429,7 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr { name: "id", label: t("Print QR Code"), - onClick: () => {}, + onClick: handlePrintClick, buttonIcon: , align: "center", headerAlign: "center", @@ -190,9 +456,11 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr // } // }, ], - [t, onDetailClick, downloadQrCode, handleStockTransfer], + [t, onDetailClick, downloadQrCode, handleStockTransfer, handlePrintClick], ); + + const handleCloseStockTransferModal = useCallback(() => { setStockTransferModalOpen(false); setSelectedLotLine(null); @@ -234,7 +502,31 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr }, [selectedLotLine, targetLocation, qtyToBeTransferred, handleCloseStockTransferModal, setIsUploading, t, onStockTransferSuccess]); return <> - {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} + + + {inventory ? `${t("Item selected")}: ${inventory.itemCode} | ${inventory.itemName} (${t(inventory.itemType)})` : t("No items are selected yet.")} + +{inventory && ( + } + 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', + }, + }} + /> +)} + + items={availableLotLines} columns={columns} @@ -428,7 +720,343 @@ const InventoryLotLineTable: React.FC = ({ inventoryLotLines, pagingContr - + setPrintModalOpen(false)} + sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} +> + + + {t("Print QR Code")} + setPrintModalOpen(false)}> + + + + 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) => } + /> + + + setPrintQty(Math.max(1, parseInt(e.target.value) || 1))} + inputProps={{ min: 1 }} + fullWidth + /> + + + + + + + + setStockAdjustmentModalOpen(false)} + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }} +> + + + +{inventory + ? `${t("Edit mode")}: ${inventory.itemCode} ${inventory.itemName}` + : t("Stock Adjustment") +} + + setStockAdjustmentModalOpen(false)}> + + + + + + + + + +{/* List view */} + + + + + {t("Lot No")} + {t("Original Qty")} + {t("Adjusted Qty")} + {t("Difference")} + {t("Stock UoM")} + {t("Expiry Date")} + {t("Location")} + {t("Remarks")} + {t("Action")} + + + + {adjustmentEntries.map((line) => ( + + + + + {line.lotNo?.trim() ? line.lotNo : t("No lot no entered, will be generated by system.")} + {line.isOpeningInventory && ` (${t("Opening Inventory")})`} + + {line.productlotNo && {t("productLotNo")}: {line.productlotNo}} + {line.dnNo && {t("dnNo")}: {line.dnNo}} + + + {line.originalQty ?? 0} + + { + 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, + }, + }} + /> + + + {(() => { + 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 {text}; + })()} + + {line.uom} + {arrayToDateString(line.expiryDate)} + {line.warehouse?.code ?? ""} + + {pendingRemovalLineId === line.id ? ( + 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 ? ( + 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} + + + {pendingRemovalLineId === line.id ? ( + + + + + ) : ( + handleRemoveClick(line.id)} + color="error" + title={t("Remove")} + > + + + )} + + + ))} + +
+
+
+
+ + setAddEntryModalOpen(false)} + sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} +> + + + {t("Add entry")} + setAddEntryModalOpen(false)}> + + + + setAddEntryForm(f => ({ ...f, qty: Math.max(0, parseInt(e.target.value) || 0) }))} inputProps={{ min: 0 }} /> + + + + + + + setAddEntryForm(f => ({ ...f, expiryDate: value ? dayjs(value).format(INPUT_DATE_FORMAT) : '' }))} slotProps={{ textField: { fullWidth: true, required: true } }} /> + + + + 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) => } /> + + + setAddEntryForm(f => ({ ...f, isOpeningInventory: e.target.checked }))} />} label={t("Opening Inventory")} /> + + + setAddEntryForm(f => ({ ...f, productlotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> + + + setAddEntryForm(f => ({ ...f, dnNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> + + + setAddEntryForm(f => ({ ...f, lotNo: e.target.value }))} sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} /> + + + setAddEntryForm(f => ({ ...f, remarks: e.target.value }))} + multiline + minRows={2} + sx={{ '& .MuiInputBase-input::placeholder': { color: '#9e9e9e', opacity: 1 } }} + /> + + + + + + + + + } export default InventoryLotLineTable; \ No newline at end of file diff --git a/src/components/InventorySearch/InventorySearch.tsx b/src/components/InventorySearch/InventorySearch.tsx index dcb4b2e..3451a93 100644 --- a/src/components/InventorySearch/InventorySearch.tsx +++ b/src/components/InventorySearch/InventorySearch.tsx @@ -10,9 +10,11 @@ import InventoryTable from "./InventoryTable"; import { defaultPagingController } from "../SearchResults/SearchResults"; import InventoryLotLineTable from "./InventoryLotLineTable"; import { SearchInventory, SearchInventoryLotLine, fetchInventories, fetchInventoryLotLines } from "@/app/api/inventory/actions"; +import { PrinterCombo } from "@/app/api/settings/printer"; interface Props { inventories: InventoryResult[]; + printerCombo?: PrinterCombo[]; } type SearchQuery = Partial< @@ -32,7 +34,7 @@ type SearchQuery = Partial< >; type SearchParamNames = keyof SearchQuery; -const InventorySearch: React.FC = ({ inventories }) => { +const InventorySearch: React.FC = ({ inventories, printerCombo }) => { const { t } = useTranslation(["inventory", "common"]); // Inventory @@ -58,6 +60,7 @@ const InventorySearch: React.FC = ({ inventories }) => { currencyName: "", status: "", baseUom: "", + uomShortDesc: "", }), []) const [inputs, setInputs] = useState>(defaultInputs); @@ -248,9 +251,13 @@ const InventorySearch: React.FC = ({ inventories }) => { setPagingController={setInventoryLotLinesPagingController} totalCount={inventoryLotLinesTotalCount} inventory={selectedInventory} + printerCombo={printerCombo ?? []} onStockTransferSuccess={() => refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController) } + onStockAdjustmentSuccess={() => + refetchInventoryLotLineData(selectedInventory?.itemId ?? null, "search", inventoryLotLinesPagingController) + } /> ); diff --git a/src/components/InventorySearch/InventorySearchWrapper.tsx b/src/components/InventorySearch/InventorySearchWrapper.tsx index 63dd5ca..cffbfc5 100644 --- a/src/components/InventorySearch/InventorySearchWrapper.tsx +++ b/src/components/InventorySearch/InventorySearchWrapper.tsx @@ -2,15 +2,19 @@ import React from "react"; import GeneralLoading from "../General/GeneralLoading"; import { fetchInventories } from "@/app/api/inventory"; import InventorySearch from "./InventorySearch"; +import { fetchPrinterCombo } from "@/app/api/settings/printer"; interface SubComponents { Loading: typeof GeneralLoading; } const InventorySearchWrapper: React.FC & SubComponents = async () => { - const [inventories] = await Promise.all([fetchInventories()]); + const [inventories, printerCombo] = await Promise.all([ + fetchInventories(), + fetchPrinterCombo(), + ]); - return ; + return ; }; InventorySearchWrapper.Loading = GeneralLoading; diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index a222604..2148c2a 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -218,6 +218,27 @@ "Target Location": "目標倉位", "Original Qty": "原有數量", "Qty To Be Transferred": "待轉數量", - "Submit": "提交" + "Submit": "提交", + + "Printer": "列印機", + "Print Qty": "列印數量", + "Print": "列印", + "Print sent": "已送出列印", + "Print failed": "列印失敗", + + "Stock Adjustment": "庫存調整", + "Edit mode": "編輯模式", + "Add entry": "新增倉存", + + "productLotNo": "產品批號", + "dnNo": "送貨單編號", + "Optional - system will generate": "選填,系統將自動生成", + "Add": "新增", + "Opening Inventory": "開倉", + "Reason for adjustment": "調整原因", + "No lot no entered, will be generated by system.": "未輸入批號,將由系統生成。", + "Reason for removal": "移除原因", + "Confirm remove": "確認移除", + "Adjusted Qty": "調整後倉存" }