diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index bca0a75..675380c 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -287,10 +287,10 @@ export default function ProductionSchedulePage() { - {/* Detail Modal */} + {/* Detail Modal – z-index above sidebar drawer (1200) so they don't overlap on small windows */} {isDetailOpen && (
@@ -503,7 +503,7 @@ export default function ProductionSchedulePage() { {/* Export Dialog */} {isExportDialogOpen && (
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/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 790c4fe..5d52d32 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -25,6 +25,7 @@ export interface StockIssueResult { handleStatus: string; handleDate: string | null; handledBy: number | null; + uomDesc: string | null; } export interface ExpiryItemResult { id: number; @@ -178,6 +179,8 @@ export async function submitMissItem(issueId: number, handler: number) { itemDescription: string | null; storeLocation: string | null; issues: IssueDetailItem[]; + bookQty: number; + uomDesc: string | null; } export interface IssueDetailItem { diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index e54376a..83403a5 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -40,6 +40,7 @@ export interface InventoryLotDetailResponse { approverQty: number | null; approverBadQty: number | null; finalQty: number | null; + bookQty: number | null; } export const getInventoryLotDetailsBySection = async ( @@ -207,6 +208,7 @@ export interface BatchSaveApproverStockTakeRecordRequest { stockTakeId: number; stockTakeSection: string; approverId: number; + variancePercentTolerance?: number | null; } export interface BatchSaveApproverStockTakeRecordResponse { @@ -312,7 +314,10 @@ export const getInventoryLotDetailsBySectionNotMatch = async ( ); return response; } - +export interface SearchStockTransactionResult { + records: StockTransactionResponse[]; + total: number; +} export interface SearchStockTransactionRequest { startDate: string | null; endDate: string | null; @@ -345,7 +350,6 @@ export interface StockTransactionListResponse { } export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => { - // 构建查询字符串 const params = new URLSearchParams(); if (request.itemCode) params.append("itemCode", request.itemCode); @@ -366,7 +370,10 @@ export const searchStockTransactions = cache(async (request: SearchStockTransact next: { tags: ["Stock Transaction List"] }, } ); - // 确保返回正确的格式 - return response?.records || []; + // 回傳 records 與 total,供分頁正確顯示 + return { + records: response?.records || [], + total: response?.total ?? 0, + }; }); diff --git a/src/app/global.css b/src/app/global.css index 261f31c..ab3976f 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -29,9 +29,46 @@ body { overscroll-behavior: none; } +/* Tablet/mobile: stable layout when virtual keyboard opens */ +html { + /* Prefer dynamic viewport height so layout can adapt to keyboard (if browser resizes) */ + height: 100%; + /* Base font size: slightly larger for readability */ + font-size: 16px; +} +@media (min-width: 640px) { + html { + font-size: 17px; + } +} +@media (min-width: 1024px) { + html { + font-size: 18px; + } +} body { + min-height: 100%; + min-height: 100dvh; background-color: var(--background); color: var(--foreground); + font-size: 1rem; + line-height: 1.6; +} + +/* Full-height containers: use dvh so keyboard doesn’t squash the layout when overlay is used */ +@media (max-width: 1024px) { + .min-h-screen { + min-height: 100dvh; + } +} + +/* Avoid iOS zoom on input focus (keep inputs ≥16px where possible) */ +@media (max-width: 1024px) { + input, + select, + textarea { + font-size: max(16px, 1rem); + } } .app-search-criteria { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dde610b..027afb7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; // import { detectLanguage } from "@/i18n"; // import ThemeRegistry from "@/theme/ThemeRegistry"; import { detectLanguage } from "../i18n"; @@ -9,6 +9,14 @@ export const metadata: Metadata = { description: "FPSMS - xxxx Management System", }; +/** Tablet/mobile: virtual keyboard overlays content instead of resizing viewport (avoids "half screen gone"). */ +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + interactiveWidget: "overlays-content", +}; + export default async function RootLayout({ children, }: { diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 0ac5884..066d65c 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -14,6 +14,7 @@ const pathToLabelMap: { [path: string]: string } = { "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/settings/qcItem": "Qc Item", + "/settings/qcItemAll": "QC Item All", "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", "/settings/equipment": "Equipment", diff --git a/src/components/General/LoadingComponent.tsx b/src/components/General/LoadingComponent.tsx index fc802b2..868d187 100644 --- a/src/components/General/LoadingComponent.tsx +++ b/src/components/General/LoadingComponent.tsx @@ -8,7 +8,7 @@ export const LoadingComponent: React.FC = () => { display="flex" justifyContent="center" alignItems="center" - // autoheight="true" + > diff --git a/src/components/InputDataGrid/InputDataGrid.tsx b/src/components/InputDataGrid/InputDataGrid.tsx index 51ebecf..bab891d 100644 --- a/src/components/InputDataGrid/InputDataGrid.tsx +++ b/src/components/InputDataGrid/InputDataGrid.tsx @@ -370,7 +370,7 @@ function InputDataGrid({ // columns={!checkboxSelection ? _columns : columns} columns={needActions ? _columns : columns} editMode="row" - // autoHeight + sx={{ height: "30vh", "--DataGrid-overlayHeight": "100px", 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 3296886..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 @@ -249,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/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index c9931c7..6655a92 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -23,7 +23,7 @@ import { SessionWithTokens } from "@/config/authConfig"; import { createStockInLine } from "@/app/api/stockIn/actions"; import { msg } from "../Swal/CustomAlerts"; import dayjs from "dayjs"; -import { fetchInventories } from "@/app/api/inventory/actions"; +//import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { PrinterCombo } from "@/app/api/settings/printer"; import { JobTypeResponse } from "@/app/api/jo/actions"; @@ -76,16 +76,21 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT useEffect(() => { const fetchDetailedJos = async () => { const detailedMap = new Map(); - - for (const jo of filteredJos) { - try { - const detailedJo = await fetchJoDetailClient(jo.id); - detailedMap.set(jo.id, detailedJo); - } catch (error) { - console.error(`Error fetching detail for JO ${jo.id}:`, error); - } + try { + const results = await Promise.all( + filteredJos.map((jo) => + fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => { + console.error(`Error fetching detail for JO ${jo.id}:`, error); + return null; + }) + ) + ); + results.forEach((r) => { + if (r) detailedMap.set(r.id, r.detail); + }); + } catch (error) { + console.error("Error fetching JO details:", error); } - setDetailedJos(detailedMap); }; @@ -93,7 +98,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchDetailedJos(); } }, [filteredJos]); - +/* useEffect(() => { const fetchInventoryData = async () => { try { @@ -102,9 +107,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT name: "", type: "", pageNum: 0, - pageSize: 1000 + pageSize: 200, }); - setInventoryData(inventoryResponse.records); + setInventoryData(inventoryResponse.records ?? []); } catch (error) { console.error("Error fetching inventory data:", error); } @@ -112,6 +117,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchInventoryData(); }, []); + */ const getStockAvailable = (pickLine: JoDetailPickLine) => { const inventory = inventoryData.find(inventory => diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx index e076661..5f5a0a8 100644 --- a/src/components/Jodetail/FInishedJobOrderRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -509,9 +509,11 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { size="small" sx={{ mb: 1 }} /> + {/* {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} + */} { type: undefined, status: undefined, itemName: undefined, + pageNum: 0, + pageSize: 50, }), fetchPrinterCombo(), ]); diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index 6d6a054..80b18e2 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -91,7 +91,9 @@ const PickExecutionForm: React.FC = ({ const [handlers, setHandlers] = useState>([]); const [verifiedQty, setVerifiedQty] = useState(0); const { data: session } = useSession() as { data: SessionWithTokens | null }; - + const missSet = formData.missQty != null; +const badItemSet = formData.badItemQty != null; +const badPackageSet = (formData as any).badPackageQty != null; const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { return lot.availableQty || 0; }, []); @@ -162,9 +164,9 @@ useEffect(() => { storeLocation: selectedLot.location, requiredQty: selectedLot.requiredQty, actualPickQty: initialVerifiedQty, - missQty: 0, - badItemQty: 0, - badPackageQty: 0, // Bad Package Qty (frontend only) + missQty: undefined, + badItemQty: undefined, + badPackageQty: undefined, issueRemark: "", pickerName: "", handledBy: undefined, @@ -195,10 +197,10 @@ useEffect(() => { const newErrors: FormErrors = {}; const ap = Number(verifiedQty) || 0; const miss = Number(formData.missQty) || 0; - const badItem = Number(formData.badItemQty) || 0; - const badPackage = Number((formData as any).badPackageQty) || 0; - const totalBad = badItem + badPackage; - const total = ap + miss + totalBad; + const badItem = Number(formData.badItemQty) ?? 0; + const badPackage = Number((formData as any).badPackageQty) ?? 0; + const totalBadQty = badItem + badPackage; + const total = ap + miss + totalBadQty; const availableQty = selectedLot?.availableQty || 0; // 1. Check actualPickQty cannot be negative @@ -231,7 +233,7 @@ useEffect(() => { } // 5. At least one field must have a value - if (ap === 0 && miss === 0 && totalBad === 0) { + if (ap === 0 && miss === 0 && totalBadQty === 0) { newErrors.actualPickQty = t("Enter pick qty or issue qty"); } @@ -245,10 +247,9 @@ useEffect(() => { // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程 const badPackageQty = Number((formData as any).badPackageQty) || 0; - const isNormalPick = verifiedQty > 0 - && formData.missQty == 0 - && formData.badItemQty == 0 - && badPackageQty == 0; + const isNormalPick = (formData.missQty == null || formData.missQty === 0) + && (formData.badItemQty == null || formData.badItemQty === 0) + && (badPackageQty === 0); if (isNormalPick) { if (onNormalPickSubmit) { @@ -288,11 +289,12 @@ useEffect(() => { const submissionData: PickExecutionIssueData = { ...(formData as PickExecutionIssueData), actualPickQty: verifiedQty, - lotId: formData.lotId || selectedLot?.lotId || 0, - lotNo: formData.lotNo || selectedLot?.lotNo || '', - pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '', - pickerName: session?.user?.name || '', - badItemQty: totalBadQty, + lotId: formData.lotId ?? selectedLot?.lotId ?? 0, + lotNo: formData.lotNo ?? selectedLot?.lotNo ?? '', + pickOrderCode: formData.pickOrderCode ?? selectedPickOrderLine?.pickOrderCode ?? '', + pickerName: session?.user?.name ?? '', + missQty: formData.missQty ?? 0, // 这里:null/undefined → 0 + badItemQty: totalBadQty, // totalBadQty 下面用 ?? 0 算 badReason, }; @@ -397,7 +399,8 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.missQty || 0} + disabled={badItemSet || badPackageSet} + value={formData.missQty || ""} onChange={(e) => { handleInputChange( "missQty", @@ -421,7 +424,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.badItemQty || 0} + value={formData.badItemQty || ""} onChange={(e) => { const newBadItemQty = e.target.value === "" ? undefined @@ -429,6 +432,7 @@ useEffect(() => { handleInputChange('badItemQty', newBadItemQty); }} error={!!errors.badItemQty} + disabled={missSet || badPackageSet} helperText={errors.badItemQty} variant="outlined" /> @@ -444,7 +448,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={(formData as any).badPackageQty || 0} + value={(formData as any).badPackageQty || ""} onChange={(e) => { handleInputChange( "badPackageQty", @@ -453,6 +457,7 @@ useEffect(() => { : Math.max(0, Number(e.target.value) || 0) ); }} + disabled={missSet || badItemSet} error={!!errors.badItemQty} variant="outlined" /> diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index 823f79c..21163e1 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -868,7 +868,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { qty: submitQty, isMissing: false, isBad: false, - reason: undefined + reason: undefined, + userId: currentUserId ?? 0 } ); @@ -881,7 +882,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { } catch (error) { console.error("Error submitting second scan quantity:", error); } - }, [fetchJobOrderData]); + }, [fetchJobOrderData, currentUserId]); const handlePickExecutionForm = useCallback((lot: any) => { console.log("=== Pick Execution Form ==="); @@ -1263,55 +1264,24 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} - {/* - - {lot.matchStatus?.toLowerCase() === 'scanned' || - lot.matchStatus?.toLowerCase() === 'completed' ? ( - - - - ) : ( - - {t(" ")} - - )} - - */} + - - ))} @@ -675,6 +672,7 @@ const CompleteJobOrderRecord: React.FC = ({ onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[5, 10, 25, 50]} + labelRowsPerPage={t("Rows per page")} /> )} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 4549061..3b9a9d8 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -1822,7 +1822,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [handleSubmitPickQtyWithQty]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => - lot.stockOutLineStatus === 'checked' + lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'partially_completed' ); if (scannedLots.length === 0) { diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index 73f4c8a..7be50c8 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -8,19 +8,33 @@ import { Box } from "@mui/material"; const LoginPage = () => { return ( - - - + + - + { display: "flex", alignItems: "flex-end", justifyContent: "center", - svg: { maxHeight: 120 }, + backgroundImage: "linear-gradient(135deg, rgba(59,130,246,0.15) 0%, #f1f5f9 45%, #f8fafc 100%)", + backgroundColor: "#f8fafc", }} > - + diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index 8e18be2..f8bc458 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -6,14 +6,18 @@ interface Props { className?: string; } +/** Same logo height everywhere so login and main page look identical. */ +const DEFAULT_LOGO_HEIGHT = 42; + /** - * Logo: 3D-style badge (FP) + MTMS wordmark. - * Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name. + * Logo: rounded badge (FP) with links motif inside + FP-MTMS wordmark. + * Uses fixed typography so words look the same on login and main page. */ -const Logo: React.FC = ({ height = 44, className = "" }) => { +const Logo: React.FC = ({ height = DEFAULT_LOGO_HEIGHT, className = "" }) => { const size = Math.max(28, height); const badgeSize = Math.round(size * 0.7); - const fontSize = Math.round(size * 0.5); + const titleFontSize = 21; + const subtitleFontSize = 10; const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14; return ( @@ -22,7 +26,7 @@ const Logo: React.FC = ({ height = 44, className = "" }) => { style={{ display: "flex", flexShrink: 0 }} aria-label="FP-MTMS" > - {/* 3D badge: FP with gradient, top bevel, and soft shadow */} + {/* Badge: rounded square with links motif inside + FP */} = ({ height = 44, className = "" }) => { aria-hidden > - {/* Energetic blue gradient: bright top → deep blue bottom */} - + - {/* Shadow layer - deep blue */} - - {/* Main 3D body */} - - {/* Top bevel (inner 3D) */} - - {/* FP text */} + {/* Shadow */} + + {/* Body */} + + + {/* Links motif inside: small chain links in corners, clear center for FP */} + + + + + + + + + + + {/* FP text – top-right so it doesn’t overlap the links */} = ({ height = 44, className = "" }) => { FP - {/* Wordmark: MTMS + subtitle — strong, energetic */} -
+ {/* Wordmark: fixed typography so login and main page match */} +
- MTMS + FP-MTMS Food Production diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index f47d72a..18e9072 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -96,7 +96,7 @@ const NavigationContent: React.FC = () => { { icon: , label: "Stock Take Management", - requiredAbility: [AUTH.STOCK_TAKE, AUTH.ADMIN], + requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN], path: "/stocktakemanagement", }, { @@ -120,7 +120,7 @@ const NavigationContent: React.FC = () => { { icon: , label: "Stock Record", - requiredAbility: [AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN], + requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN], path: "/stockRecord", }, ], diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index 115a86b..b9cb8d9 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -525,7 +525,7 @@ const closeNewModal = useCallback(() => { width: 150, // flex: 0.5, renderCell: (params) => { - return params.row.uom?.udfudesc; + return itemDetail.uom?.udfudesc; }, }, { diff --git a/src/components/ProductionProcess/BagConsumptionForm.tsx b/src/components/ProductionProcess/BagConsumptionForm.tsx index afee258..2de6881 100644 --- a/src/components/ProductionProcess/BagConsumptionForm.tsx +++ b/src/components/ProductionProcess/BagConsumptionForm.tsx @@ -140,7 +140,17 @@ const BagConsumptionForm: React.FC = ({ alert(t("Please select at least one bag")); return; } - + for (const row of validRows) { + const selectedBag = bagList.find((b) => b.id === row.bagLotLineId); + const available = selectedBag?.balanceQty ?? 0; + const requested = row.consumedQty + row.scrapQty; + if (requested > available) { + alert( + `${selectedBag?.bagName ?? "Bag"}: ${t("Insufficient balance")}. ${t("Available")}: ${available}, ${t("Requested")}: ${requested}` + ); + return; + } + } // 提交每个 bag consumption const promises = validRows.map((row) => { const selectedBag = bagList.find((b) => b.id === row.bagLotLineId); diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index b569b70..f00ddf5 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -422,7 +422,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { const productionProcessesLineRemarkTableColumns: GridColDef[] = [ { field: "seqNo", - headerName: t("Seq"), + headerName: t("SEQ"), flex: 0.2, align: "left", headerAlign: "left", diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 2190567..dcdde39 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -220,7 +220,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess display: "flex", flexDirection: "column", border: "1px solid", - borderColor: "success.main", + borderColor: "blue", }} > = ({ onSelectProcess - - {t("Item Name")}: {process.itemCode} {process.itemName} + + {/* {t("Item Name")}: */} + {process.itemCode} {process.itemName} {t("Production Priority")}: {process.productionPriority} @@ -306,7 +307,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess )} {statusLower === "completed" && ( - )} diff --git a/src/components/PutAwayScan/PutAwayModal.tsx b/src/components/PutAwayScan/PutAwayModal.tsx index d14df1b..4119ed9 100644 --- a/src/components/PutAwayScan/PutAwayModal.tsx +++ b/src/components/PutAwayScan/PutAwayModal.tsx @@ -57,17 +57,16 @@ const style = { left: "50%", transform: "translate(-50%, -50%)", bgcolor: "background.paper", - pt: { xs: 0.5, sm: 1, md: 1.5 }, - px: { xs: 1, sm: 1.5, md: 2 }, - pb: { xs: 0.5, sm: 1, md: 1.5 }, - width: { xs: "95%", sm: "85%", md: "75%", lg: "70%" }, - maxWidth: "900px", - maxHeight: { xs: "98vh", sm: "95vh", md: "90vh" }, + pt: { xs: 0.5, sm: 0.75, md: 1 }, + px: { xs: 1, sm: 1, md: 1.5 }, + pb: { xs: 0.5, sm: 0.75, md: 1 }, + width: { xs: "95%", sm: "72%", md: "60%", lg: "70%" }, + maxWidth: "720px", + maxHeight: { xs: "98vh", sm: "92vh", md: "88vh" }, overflow: "hidden", display: "flex", flexDirection: "column", }; - const scannerStyle = { position: "absolute", top: "50%", @@ -442,9 +441,9 @@ const PutAwayModal: React.FC = ({ open, onClose, warehouse, stockInLineId {itemDetail != undefined ? ( <> - - 處理上架 - + + 處理上架 + {itemDetail.jobOrderId ? ( diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx index 10d46d4..8fac0e4 100644 --- a/src/components/Qc/QcForm.tsx +++ b/src/components/Qc/QcForm.tsx @@ -232,12 +232,23 @@ const QcForm: React.FC = ({ rows, disabled = false }) => { return ( <> + 'auto'} + initialState={{ + pagination: { paginationModel: { page: 0, pageSize: 100 } }, + }} + pageSizeOptions={[100]} + slotProps={{ + pagination: { + sx: { + display: "none", + }, + }, + }} /> ); diff --git a/src/components/Qc/QcStockInModal.tsx b/src/components/Qc/QcStockInModal.tsx index 3743a6f..ce55928 100644 --- a/src/components/Qc/QcStockInModal.tsx +++ b/src/components/Qc/QcStockInModal.tsx @@ -68,6 +68,7 @@ interface CommonProps extends Omit { interface Props extends CommonProps { // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; } + const QcStockInModal: React.FC = ({ open, onClose, @@ -94,6 +95,10 @@ const QcStockInModal: React.FC = ({ () => `qcStockInModal_selectedPrinterId_${session?.id ?? "guest"}`, [session?.id], ); + const labelPrinterCombo = useMemo( + () => (printerCombo || []).filter((p) => p.type === "Label"), + [printerCombo], + ); const getDefaultPrinter = useMemo(() => { if (!printerCombo.length) return undefined; if (typeof window === "undefined") return printerCombo[0]; @@ -102,7 +107,7 @@ const QcStockInModal: React.FC = ({ const matched = savedId ? printerCombo.find(p => p.id === Number(savedId)) : undefined; return matched ?? printerCombo[0]; }, [printerCombo, printerStorageKey]); - const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); + const [selectedPrinter, setSelectedPrinter] = useState(labelPrinterCombo[0]); const [printQty, setPrintQty] = useState(1); const [tabIndex, setTabIndex] = useState(0); @@ -504,6 +509,7 @@ const QcStockInModal: React.FC = ({ // Put away model const [pafRowModesModel, setPafRowModesModel] = useState({}) const [pafRowSelectionModel, setPafRowSelectionModel] = useState([]) + const pafSubmitDisable = useMemo(() => { return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit) }, [pafRowModesModel]) @@ -749,21 +755,25 @@ const printQrcode = useCallback( {tabIndex == 1 && ( { - setSelectedPrinter(value) - }} - renderInput={(params) => ( - - )} - /> + disableClearable + options={labelPrinterCombo} + getOptionLabel={(option) => + option.name || option.label || option.code || `Printer ${option.id}` + } + value={selectedPrinter} + onChange={(_, newValue) => { + if (newValue) setSelectedPrinter(newValue); + }} + renderInput={(params) => ( + + )} + /> ({ setCheckboxIds = undefined, onRowClick = undefined, }: Props) { - const { t } = useTranslation("dashboard"); + const { t } = useTranslation(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); diff --git a/src/components/StockIssue/SearchPage.tsx b/src/components/StockIssue/SearchPage.tsx index d14b622..5a0836f 100644 --- a/src/components/StockIssue/SearchPage.tsx +++ b/src/components/StockIssue/SearchPage.tsx @@ -169,7 +169,17 @@ const SearchPage: React.FC = ({ dataList }) => { { name: "itemDescription", label: t("Item") }, { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, + { + name: "bookQty", + label: t("Book Qty"), + renderCell: (item) => ( + <>{item.bookQty?.toFixed(2) ?? "0"} {item.uomDesc ?? ""} + ), + }, { name: "issueQty", label: t("Miss Qty") }, + { name: "uomDesc", label: t("UoM"), renderCell: (item) => ( + <>{item.uomDesc ?? ""} + ) }, { name: "id", label: t("Action"), @@ -196,6 +206,9 @@ const SearchPage: React.FC = ({ dataList }) => { { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, { name: "issueQty", label: t("Defective Qty") }, + { name: "uomDesc", label: t("UoM"), renderCell: (item) => ( + <>{item.uomDesc ?? ""} + ) }, { name: "id", label: t("Action"), diff --git a/src/components/StockIssue/SubmitIssueForm.tsx b/src/components/StockIssue/SubmitIssueForm.tsx index 37d6bab..1a891fe 100644 --- a/src/components/StockIssue/SubmitIssueForm.tsx +++ b/src/components/StockIssue/SubmitIssueForm.tsx @@ -49,7 +49,10 @@ const SubmitIssueForm: React.FC = ({ const [submitting, setSubmitting] = useState(false); const [details, setDetails] = useState(null); const [submitQty, setSubmitQty] = useState(""); - + const bookQty = details?.bookQty ?? 0; + const submitQtyNum = parseFloat(submitQty); + const submitQtyValid = !Number.isNaN(submitQtyNum) && submitQtyNum >= 0; + const remainAvailable = submitQtyValid ? Math.max(0, bookQty - submitQtyNum) : bookQty; useEffect(() => { if (open && lotId) { loadDetails(); @@ -121,9 +124,17 @@ const SubmitIssueForm: React.FC = ({ {t("Lot No.")}: {details.lotNo} - + {t("Location")}: {details.storeLocation} + + {t("Book Qty")}:{" "} + {details.bookQty} + + + {t("UoM")}:{" "} + {details.uomDesc ?? ""} + @@ -146,8 +157,8 @@ const SubmitIssueForm: React.FC = ({ {issue.pickerName || "-"} {issueType === "miss" - ? issue.missQty?.toFixed(2) || "0" - : issue.issueQty?.toFixed(2) || "0"} + ? issue.missQty?.toFixed(0) || "0" + : issue.issueQty?.toFixed(0) || "0"} {issue.pickOrderCode} {issue.doOrderCode || "-"} @@ -168,6 +179,26 @@ const SubmitIssueForm: React.FC = ({ inputProps={{ min: 0, step: 0.01 }} sx={{ mt: 2 }} /> + { + const raw = e.target.value; + if (raw === "") { + setSubmitQty(""); + return; + } + const remain = parseFloat(raw); + if (!Number.isNaN(remain) && remain >= 0) { + const newSubmit = Math.max(0, bookQty - remain); + setSubmitQty(newSubmit.toFixed(0)); + } + }} + inputProps={{ min: 0, step: 0.01, readOnly: false }} + sx={{ mt: 2 }} + /> - - + {loadingDetails ? ( @@ -454,9 +495,10 @@ const ApproverStockTake: React.FC = ({ {t("Warehouse Location")} {t("Item-lotNo-ExpiryDate")} + {t("UOM")} {t("Stock Take Qty(include Bad Qty)= Available Qty")} {t("Remark")} - {t("UOM")} + {t("Record Status")} {t("Action")} @@ -492,25 +534,27 @@ const ApproverStockTake: React.FC = ({ {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} - + {detail.uom || "-"} - {detail.finalQty != null ? ( - - {(() => { - const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0); - const differenceColor = finalDifference > 0 - ? 'error.main' - : finalDifference < 0 - ? 'error.main' - : 'success.main'; - - return ( - - {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)} - - ); - })()} - + {detail.finalQty != null ? ( + + {(() => { + // 若有 bookQty(盤點當時帳面),用它來算差異;否則用 availableQty + const bookQtyToUse = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); + const finalDifference = (detail.finalQty || 0) - bookQtyToUse; + const differenceColor = detail.stockTakeRecordStatus === "completed" + ? 'text.secondary' + : finalDifference !== 0 + ? 'error.main' + : 'success.main'; + + return ( + + {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(bookQtyToUse)} = {formatNumber(finalDifference)} + + ); + })()} + ) : ( {hasFirst && ( @@ -581,7 +625,7 @@ const ApproverStockTake: React.FC = ({ disabled={selection !== "approver"} /> - ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} + = {formatNumber(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} )} @@ -597,12 +641,12 @@ const ApproverStockTake: React.FC = ({ selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))|| 0; } - const bookQty = detail.availableQty || 0; + const bookQty = detail.bookQty != null ? detail.bookQty : (detail.availableQty || 0); const difference = selectedQty - bookQty; - const differenceColor = difference > 0 - ? 'error.main' - : difference < 0 - ? 'error.main' + const differenceColor = detail.stockTakeRecordStatus === "completed" + ? 'text.secondary' + : difference !== 0 + ? 'error.main' : 'success.main'; return ( @@ -621,11 +665,13 @@ const ApproverStockTake: React.FC = ({ - {detail.uom || "-"} + - {detail.stockTakeRecordStatus === "pass" ? ( + {detail.stockTakeRecordStatus === "completed" ? ( + ) : detail.stockTakeRecordStatus === "pass" ? ( + ) : detail.stockTakeRecordStatus === "notMatch" ? ( ) : ( diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index 15c437a..6c6f0ca 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -13,6 +13,11 @@ import { TablePagination, Grid, LinearProgress, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, } from "@mui/material"; import { useState, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; @@ -41,7 +46,7 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT const [stockTakeSessions, setStockTakeSessions] = useState([]); const [page, setPage] = useState(0); const [creating, setCreating] = useState(false); - + const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const fetchStockTakeSessions = useCallback(async () => { setLoading(true); try { @@ -64,6 +69,7 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); const handleCreateStockTake = useCallback(async () => { + setOpenConfirmDialog(false); setCreating(true); try { const result = await createStockTakeForSections(); @@ -177,7 +183,7 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT + + + ); }; diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index 9233ca8..1ff1001 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -21,7 +21,6 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, - getInventoryLotDetailsBySection, InventoryLotDetailResponse, saveStockTakeRecord, SaveStockTakeRecordRequest, @@ -51,13 +50,13 @@ const PickerReStockTake: React.FC = ({ const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); - // 编辑状态 - const [editingRecord, setEditingRecord] = useState(null); - const [firstQty, setFirstQty] = useState(""); - const [secondQty, setSecondQty] = useState(""); - const [firstBadQty, setFirstBadQty] = useState(""); - const [secondBadQty, setSecondBadQty] = useState(""); - const [remark, setRemark] = useState(""); + const [recordInputs, setRecordInputs] = useState>({}); const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); @@ -115,28 +114,36 @@ const PickerReStockTake: React.FC = ({ } }, [selectedSession, total]); + useEffect(() => { + const inputs: Record = {}; + inventoryLotDetails.forEach((detail) => { + const firstTotal = detail.firstStockTakeQty != null + ? (detail.firstStockTakeQty + (detail.firstBadQty ?? 0)).toString() + : ""; + const secondTotal = detail.secondStockTakeQty != null + ? (detail.secondStockTakeQty + (detail.secondBadQty ?? 0)).toString() + : ""; + inputs[detail.id] = { + firstQty: firstTotal, + secondQty: secondTotal, + firstBadQty: detail.firstBadQty?.toString() || "", + secondBadQty: detail.secondBadQty?.toString() || "", + remark: detail.remarks || "", + }; + }); + setRecordInputs(inputs); + }, [inventoryLotDetails]); + useEffect(() => { loadDetails(page, pageSize); }, [page, pageSize, loadDetails]); - - const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { - setEditingRecord(detail); - setFirstQty(detail.firstStockTakeQty?.toString() || ""); - setSecondQty(detail.secondStockTakeQty?.toString() || ""); - setFirstBadQty(detail.firstBadQty?.toString() || ""); - setSecondBadQty(detail.secondBadQty?.toString() || ""); - setRemark(detail.remarks || ""); - }, []); - - const handleCancelEdit = useCallback(() => { - setEditingRecord(null); - setFirstQty(""); - setSecondQty(""); - setFirstBadQty(""); - setSecondBadQty(""); - setRemark(""); - }, []); - + const formatNumber = (num: number | null | undefined): string => { + if (num == null || Number.isNaN(num)) return "0"; + return num.toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); + }; const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { if (!selectedSession || !currentUserId) { return; @@ -145,41 +152,69 @@ const PickerReStockTake: React.FC = ({ const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; - const qty = isFirstSubmit ? firstQty : secondQty; - const badQty = isFirstSubmit ? firstBadQty : secondBadQty; - - if (!qty || !badQty) { + // 用戶輸入為 total 和 bad,需計算 available = total - bad(與 PickerStockTake 一致) + const totalQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstQty : recordInputs[detail.id]?.secondQty; + const badQtyStr = isFirstSubmit ? recordInputs[detail.id]?.firstBadQty : recordInputs[detail.id]?.secondBadQty; + + if (!totalQtyStr) { onSnackbar( isFirstSubmit - ? t("Please enter QTY and Bad QTY") - : t("Please enter Second QTY and Bad QTY"), + ? t("Please enter QTY") + : t("Please enter Second QTY"), "error" ); return; } - + + const totalQty = parseFloat(totalQtyStr); + const badQty = parseFloat(badQtyStr || "0") || 0; + + if (Number.isNaN(totalQty)) { + onSnackbar(t("Invalid QTY"), "error"); + return; + } + + const availableQty = totalQty - badQty; + + if (availableQty < 0) { + onSnackbar(t("Available QTY cannot be negative"), "error"); + return; + } + setSaving(true); try { const request: SaveStockTakeRecordRequest = { stockTakeRecordId: detail.stockTakeRecordId || null, inventoryLotLineId: detail.id, - qty: parseFloat(qty), - badQty: parseFloat(badQty), - remark: isSecondSubmit ? (remark || null) : null, + qty: availableQty, + badQty: badQty, + remark: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : null, }; - console.log('handleSaveStockTake: request:', request); - console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); - console.log('handleSaveStockTake: currentUserId:', currentUserId); - await saveStockTakeRecord( + const result = await saveStockTakeRecord( request, selectedSession.stockTakeId, currentUserId ); onSnackbar(t("Stock take record saved successfully"), "success"); - handleCancelEdit(); - - await loadDetails(page, pageSize); + + const savedId = result?.id ?? detail.stockTakeRecordId; + setInventoryLotDetails((prev) => + prev.map((d) => + d.id === detail.id + ? { + ...d, + stockTakeRecordId: savedId ?? d.stockTakeRecordId, + firstStockTakeQty: isFirstSubmit ? availableQty : d.firstStockTakeQty, + firstBadQty: isFirstSubmit ? badQty : d.firstBadQty ?? null, + secondStockTakeQty: isSecondSubmit ? availableQty : d.secondStockTakeQty, + secondBadQty: isSecondSubmit ? badQty : d.secondBadQty ?? null, + remarks: isSecondSubmit ? (recordInputs[detail.id]?.remark || null) : d.remarks, + stockTakeRecordStatus: "pass", + } + : d + ) + ); } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); @@ -199,15 +234,13 @@ const PickerReStockTake: React.FC = ({ } finally { setSaving(false); } - }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); + }, [selectedSession, recordInputs, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { - console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); return; } - console.log('handleBatchSubmitAll: Starting batch save...'); setBatchSaving(true); try { const request: BatchSaveStockTakeRecordRequest = { @@ -217,7 +250,6 @@ const PickerReStockTake: React.FC = ({ }; const result = await batchSaveStockTakeRecords(request); - console.log('handleBatchSubmitAll: Result:', result); onSnackbar( t("Batch save completed: {{success}} success, {{errors}} errors", { @@ -273,31 +305,19 @@ const PickerReStockTake: React.FC = ({ const newInput = prev + e.key; if (newInput === '{2fitestall}') { - console.log('✅ Shortcut {2fitestall} detected!'); setTimeout(() => { if (handleBatchSubmitAllRef.current) { - console.log('Calling handleBatchSubmitAll...'); handleBatchSubmitAllRef.current().catch(err => { console.error('Error in handleBatchSubmitAll:', err); }); - } else { - console.error('handleBatchSubmitAllRef.current is null'); } }, 0); return ""; } - if (newInput.length > 15) { - return ""; - } - - if (newInput.length > 0 && !newInput.startsWith('{')) { - return ""; - } - - if (newInput.length > 5 && !newInput.startsWith('{2fi')) { - return ""; - } + if (newInput.length > 15) return ""; + if (newInput.length > 0 && !newInput.startsWith('{')) return ""; + if (newInput.length > 5 && !newInput.startsWith('{2fi')) return ""; return newInput; }); @@ -315,11 +335,15 @@ const PickerReStockTake: React.FC = ({ }, []); const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { - if (detail.stockTakeRecordStatus === "pass") { + if (selectedSession?.status?.toLowerCase() === "completed") { + return true; + } + const recordStatus = detail.stockTakeRecordStatus?.toLowerCase(); + if (recordStatus === "pass" || recordStatus === "completed") { return true; } return false; - }, []); + }, [selectedSession?.status]); const uniqueWarehouses = Array.from( new Set( @@ -328,6 +352,9 @@ const PickerReStockTake: React.FC = ({ .filter(warehouse => warehouse && warehouse.trim() !== "") ) ).join(", "); + + const defaultInputs = { firstQty: "", secondQty: "", firstBadQty: "", secondBadQty: "", remark: "" }; + return (