"use client"; import { Box, Button, Stack, Typography, Chip, CircularProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TextField, Radio, TablePagination, TableSortLabel, Card, CardContent, Grid, FormControl, InputLabel, Select, MenuItem, FormControlLabel, Tab, Tabs, ToggleButton, ToggleButtonGroup, Checkbox, Dialog, DialogTitle, DialogContent, DialogActions, } from "@mui/material"; import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Collapse } from "@mui/material"; import Accordion from "@mui/material/Accordion"; import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionDetails from "@mui/material/AccordionDetails"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, InventoryLotDetailResponse, SaveApproverStockTakeRecordRequest, saveApproverStockTakeRecord, batchSaveApproverStockTakeRecordsByIds, getApproverInventoryLotDetailsAllPending, getApproverInventoryLotDetailsAllApproved, updateStockTakeRecordStatusToNotMatch, } from "@/app/api/stockTake/actions"; import { fetchWarehouseListClient } from "@/app/api/warehouse/client"; import type { WarehouseResult } from "@/app/api/warehouse"; import { stockTakeQtyEndAdornment, StockTakeQtyWithUnit } from "./stockTakeQtyAdornment"; import StockTakeQtyGapHint from "./StockTakeQtyGapHint"; import { stockTakeHiddenOnHand, stockTakeQtyGapWarnText } from "./stockTakeQtyGapWarning"; import { useStockTakeQtyGapWarnPercent } from "./useStockTakeQtyGapWarnPercent"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, roundDownPercent } from "@/app/utils/formatUtil"; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import StyledDataGrid from "@/components/StyledDataGrid/StyledDataGrid"; import type { GridPaginationModel, GridSortModel, } from "@mui/x-data-grid"; interface ApproverStockTakeAllProps { selectedSession: AllPickedStockTakeListReponse; mode: "pending" | "approved"; onBack?: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } type QtySelectionType = "first" | "second" | "approver"; type ApproverSearchKind = "location" | "section"; type ApprovedSortKey = | "stockTakeEndTime" | "stockTakeSection" | "item" | "stockTakerName" | "variance"; const APPROVER_SEARCH_FIELD_HEIGHT = 56; const APPROVER_MENU_MAX_HEIGHT = 320; const APPROVER_LOCATION_ALL = "ALL"; const APPROVER_LOCATION_BUTTONS_PER_ROW = 15; const APPROVER_LOCATION_BUTTON_WIDTH = 80; const approverSearchFieldSx = { "& .MuiOutlinedInput-root": { height: APPROVER_SEARCH_FIELD_HEIGHT, minHeight: APPROVER_SEARCH_FIELD_HEIGHT, boxSizing: "border-box", alignItems: "center", }, "& .MuiSelect-select": { height: `${APPROVER_SEARCH_FIELD_HEIGHT}px !important`, minHeight: "0 !important", display: "flex", alignItems: "center", boxSizing: "border-box", py: "0 !important", }, } as const; const approverSearchMenuProps = { PaperProps: { sx: { maxHeight: APPROVER_MENU_MAX_HEIGHT } }, } as const; function chunkApproverOptions(items: T[], size: number): T[][] { const rows: T[][] = []; for (let i = 0; i < items.length; i += size) rows.push(items.slice(i, i + size)); return rows; } function withApproverAllOption(options: string[]): string[] { return options.length > 1 ? [APPROVER_LOCATION_ALL, ...options] : options; } function compareApproverLabel(a: string, b: string): number { return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); } function selectedApproverSectionCodes(value: string): string[] { return value.split(",").map((part) => part.trim()).filter(Boolean); } type ApproverLocationPick = { storeId: string; warehouse: string; area: string; slot: string }; function approverWarehouseKeyword(pick: ApproverLocationPick): string { const warehouse = pick.warehouse && pick.warehouse !== APPROVER_LOCATION_ALL ? pick.warehouse : ""; const area = pick.area && pick.area !== APPROVER_LOCATION_ALL ? pick.area : ""; const slot = pick.slot && pick.slot !== APPROVER_LOCATION_ALL ? pick.slot : ""; if (warehouse && area && slot) return `${warehouse}-${area}-${slot}`; if (warehouse && area) return `${warehouse}-${area}`; if (warehouse) return warehouse; if (pick.storeId && pick.storeId !== APPROVER_LOCATION_ALL) return pick.storeId; return ""; } type ApproverSearchFilters = { sectionDescription: string; stockTakeSession: string; itemKeyword: string; warehouseKeyword: string; storeId: string; status: string; variancePercentTolerance: string; varianceFilterInclusive: boolean; varianceFilterStrict: boolean; }; function defaultApproverSearchFilters(mode: "pending" | "approved"): ApproverSearchFilters { return { sectionDescription: "All", stockTakeSession: "", itemKeyword: "", warehouseKeyword: "", storeId: "All", status: mode === "pending" ? "pass" : "All", variancePercentTolerance: "5", varianceFilterInclusive: false, varianceFilterStrict: false, }; } function approverVariancePercent(detail: InventoryLotDetailResponse): number { const second = detail.secondStockTakeQty; const first = detail.firstStockTakeQty; const selected = second != null && second >= 0 ? second : first ?? 0; const book = detail.bookQty != null ? detail.bookQty : detail.availableQty ?? 0; const difference = selected - book; const raw = book !== 0 ? (difference / book) * 100 : difference === 0 ? 0 : difference > 0 ? 100 : -100; return roundDownPercent(raw); } function matchesApproverDetailFilters( detail: InventoryLotDetailResponse, filters: ApproverSearchFilters, mode: "pending" | "approved" ): boolean { if (filters.sectionDescription && filters.sectionDescription !== "All") { const desc = (detail.stockTakeSectionDescription || "").trim().toLowerCase(); if (desc !== filters.sectionDescription.trim().toLowerCase()) return false; } const sections = selectedApproverSectionCodes(filters.stockTakeSession); if (sections.length > 0) { const sec = (detail.stockTakeSection || "").trim().toLowerCase(); const hit = sections.some((part) => { const wanted = part.toLowerCase(); return sec === wanted || sec.includes(wanted); }); if (!hit) return false; } const itemParts = filters.itemKeyword.split(",").map((part) => part.trim()).filter(Boolean); if (itemParts.length > 0) { const code = detail.itemCode || ""; const name = detail.itemName || ""; const hit = itemParts.some( (part) => code.toLowerCase().includes(part.toLowerCase()) || name.toLowerCase().includes(part.toLowerCase()) ); if (!hit) return false; } const warehouseParts = filters.warehouseKeyword.split(",").map((part) => part.trim()).filter(Boolean); if (warehouseParts.length > 0) { const code = detail.warehouseCode || ""; if (!warehouseParts.some((part) => code.toLowerCase().includes(part.toLowerCase()))) return false; } if (filters.storeId && filters.storeId !== "All") { if ((detail.storeId || "").trim().toLowerCase() !== filters.storeId.trim().toLowerCase()) return false; } const statusFilter = mode === "pending" ? filters.status || "pass" : "All"; if (statusFilter !== "All") { const rowStatus = String(detail.stockTakeRecordStatus ?? "") .trim() .toLowerCase() .replaceAll("_", ""); const wanted = statusFilter.trim().toLowerCase().replaceAll("_", ""); if (rowStatus !== wanted) return false; } const tolerance = Number(filters.variancePercentTolerance); if (filters.variancePercentTolerance.trim() !== "" && Number.isFinite(tolerance) && Math.abs(tolerance) > 0) { const vp = approverVariancePercent(detail); const limit = Math.abs(tolerance); const negative = -limit; const inclusive = filters.varianceFilterInclusive; const strict = filters.varianceFilterStrict; const keep = inclusive ? strict ? vp > negative && vp < limit : vp >= negative && vp <= limit : strict ? vp < negative || vp > limit : vp <= negative || vp >= limit; if (!keep) return false; } return true; } function isBlankApproverField(value: string | undefined): boolean { return value == null || String(value).trim() === ""; } /** 審核員「盤點數」與「不良數」皆未輸入時應跳過儲存,維持 pending */ function isApproverInputsBothEmpty( detailId: number, approverQty: Record, approverBadQty: Record ): boolean { return isBlankApproverField(approverQty[detailId]) && isBlankApproverField(approverBadQty[detailId]); } type ApproverSaveBuildResult = | { ok: true; request: SaveApproverStockTakeRecordRequest; finalQty: number; finalBadQty: number; goodQty: number; selection: QtySelectionType; } | { ok: false; reason: "skip_approver_empty" } | { ok: false; reason: "error"; message: string }; function buildApproverSaveRequest( detail: InventoryLotDetailResponse, qtySelection: Record, approverQty: Record, approverBadQty: Record, currentUserId: number, t: (key: string) => string ): ApproverSaveBuildResult { const selection = qtySelection[detail.id] || "first"; let finalQty: number; let finalBadQty: number; if (selection === "first") { if (detail.firstStockTakeQty == null) { return { ok: false, reason: "error", message: t("First QTY is not available") }; } finalQty = detail.firstStockTakeQty; finalBadQty = detail.firstBadQty || 0; } else if (selection === "second") { if (detail.secondStockTakeQty == null) { return { ok: false, reason: "error", message: t("Second QTY is not available") }; } finalQty = detail.secondStockTakeQty; finalBadQty = detail.secondBadQty || 0; } else { if (isApproverInputsBothEmpty(detail.id, approverQty, approverBadQty)) { return { ok: false, reason: "skip_approver_empty" }; } const approverQtyValue = approverQty[detail.id] || "0"; const approverBadQtyValue = approverBadQty[detail.id] || "0"; finalQty = parseFloat(approverQtyValue) || 0; finalBadQty = parseFloat(approverBadQtyValue) || 0; } const request: SaveApproverStockTakeRecordRequest = { stockTakeRecordId: detail.stockTakeRecordId || null, qty: finalQty, badQty: finalBadQty, approverId: currentUserId, approverQty: selection === "approver" ? finalQty : null, approverBadQty: selection === "approver" ? finalBadQty : null, lastSelect: selection === "first" ? 1 : selection === "second" ? 2 : 3, }; const goodQty = finalQty - finalBadQty; return { ok: true, request, finalQty, finalBadQty, goodQty, selection }; } function buildBatchApproverSaveRequests( details: InventoryLotDetailResponse[], qtySelection: Record, approverQty: Record, approverBadQty: Record, currentUserId: number, t: (key: string) => string ): SaveApproverStockTakeRecordRequest[] { const records: SaveApproverStockTakeRecordRequest[] = []; for (const detail of details) { if (!detail.stockTakeRecordId || detail.stockTakeRecordId <= 0) { continue; } const built = buildApproverSaveRequest( detail, qtySelection, approverQty, approverBadQty, currentUserId, t ); if (!built.ok) { continue; } records.push(built.request); } return records; } function parseDateTimeMs( v: string | string[] | null | undefined ): number { if (v == null) return 0; if (Array.isArray(v)) { const arr = v as unknown as number[]; const [y, m, d, h = 0, min = 0, s = 0] = arr; return dayjs(`${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")} ${h}:${min}:${s}`).valueOf(); } return dayjs(v as string).valueOf(); } const ApproverStockTakeAll: React.FC = ({ selectedSession, mode, onBack, onSnackbar, }) => { const { t } = useTranslation(["stockTake", "common"]); const qtyGapWarnPercent = useStockTakeQtyGapWarnPercent(); const { data: session } = useSession() as { data: SessionWithTokens | null }; const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); const [searchVariancePercentTolerance, setSearchVariancePercentTolerance] = useState("5"); const [searchVarianceFilterInclusive, setSearchVarianceFilterInclusive] = useState(false); const [searchVarianceFilterStrict, setSearchVarianceFilterStrict] = useState(false); const [qtySelection, setQtySelection] = useState>({}); const [approverQty, setApproverQty] = useState>({}); const [gapCheckOpen, setGapCheckOpen] = useState>({}); const [approverBadQty, setApproverBadQty] = useState>({}); const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50); const [total, setTotal] = useState(0); const [approvedSortKey, setApprovedSortKey] = useState(null); const [approvedSortDir, setApprovedSortDir] = useState<"asc" | "desc">("asc"); const [locationWarehouses, setLocationWarehouses] = useState([]); const [searchKind, setSearchKind] = useState("location"); const [locationPick, setLocationPick] = useState({ storeId: "", warehouse: "", area: "", slot: "" }); const [searchSectionDescription, setSearchSectionDescription] = useState("All"); const [searchStockTakeSession, setSearchStockTakeSession] = useState(""); const [searchItemKeyword, setSearchItemKeyword] = useState(""); const [searchWarehouseKeyword, setSearchWarehouseKeyword] = useState(""); const [searchStoreId, setSearchStoreId] = useState("All"); const [searchStatus, setSearchStatus] = useState(mode === "pending" ? "pass" : "All"); const locationOwnedSectionRef = useRef(""); const [showFilters, setShowFilters] = useState(true) const [appliedFilters, setAppliedFilters] = useState(() => defaultApproverSearchFilters(mode) ); const [openBatchSaveConfirmDialog, setOpenBatchSaveConfirmDialog] = useState(false); const batchSaveInFlightRef = useRef(false); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleChangePage = useCallback((_: unknown, newPage: number) => { setPage(newPage); }, []); const handleChangeRowsPerPage = useCallback( (event: React.ChangeEvent) => { const newSize = parseInt(event.target.value, 10); if (newSize === -1) { setPageSize("all"); } else if (!isNaN(newSize)) { setPageSize(newSize); } setPage(0); }, [] ); const handleSearch = useCallback((override?: Partial & { searchKind?: ApproverSearchKind }) => { const kind = override?.searchKind ?? searchKind; const sectionDescription = kind === "location" ? "All" : (override?.sectionDescription ?? (searchSectionDescription || "All")); const stockTakeSession = kind === "location" ? (override?.stockTakeSession ?? locationOwnedSectionRef.current) : (override?.stockTakeSession ?? (searchStockTakeSession || "")); const warehouseKeyword = kind === "location" ? (override?.warehouseKeyword ?? (searchWarehouseKeyword || "")) : ""; const storeId = kind === "location" ? (override?.storeId ?? (searchStoreId || "All")) : "All"; const next: ApproverSearchFilters = { sectionDescription, stockTakeSession, itemKeyword: override?.itemKeyword ?? (searchItemKeyword || ""), warehouseKeyword, storeId, status: mode === "pending" ? (override?.status ?? searchStatus ?? "pass") : "All", variancePercentTolerance: override?.variancePercentTolerance ?? searchVariancePercentTolerance, varianceFilterInclusive: override?.varianceFilterInclusive ?? searchVarianceFilterInclusive, varianceFilterStrict: override?.varianceFilterStrict ?? searchVarianceFilterStrict, }; if (kind === "section" && override?.sectionDescription != null) setSearchSectionDescription(override.sectionDescription); if (kind === "section" && override?.stockTakeSession != null) setSearchStockTakeSession(override.stockTakeSession); if (kind === "location" && override?.warehouseKeyword != null) setSearchWarehouseKeyword(override.warehouseKeyword); if (kind === "location" && override?.storeId != null) setSearchStoreId(override.storeId); if (override?.status != null) setSearchStatus(override.status); setAppliedFilters(next); setPage(0); }, [ searchKind, searchSectionDescription, searchStockTakeSession, searchItemKeyword, searchWarehouseKeyword, searchStoreId, searchStatus, searchVariancePercentTolerance, searchVarianceFilterInclusive, searchVarianceFilterStrict, mode, ]); const handleResetSearch = useCallback(() => { const defaultStatus = mode === "pending" ? "pass" : "All"; locationOwnedSectionRef.current = ""; setLocationPick({ storeId: "", warehouse: "", area: "", slot: "" }); setSearchSectionDescription("All"); setSearchStockTakeSession(""); setSearchItemKeyword(""); setSearchWarehouseKeyword(""); setSearchStoreId("All"); setSearchStatus(defaultStatus); setSearchVariancePercentTolerance("5"); setSearchVarianceFilterInclusive(false); setSearchVarianceFilterStrict(false); setAppliedFilters(defaultApproverSearchFilters(mode)); setPage(0); }, [mode]); const switchSearchKind = useCallback((nextKind: ApproverSearchKind) => { if (nextKind === searchKind) return; setSearchKind(nextKind); if (nextKind === "location") { const specificWarehouse = Boolean(locationPick.warehouse && locationPick.warehouse !== APPROVER_LOCATION_ALL); handleSearch({ searchKind: nextKind, stockTakeSession: locationOwnedSectionRef.current, warehouseKeyword: approverWarehouseKeyword(locationPick), storeId: !specificWarehouse && locationPick.storeId && locationPick.storeId !== APPROVER_LOCATION_ALL ? locationPick.storeId : "All", }); return; } handleSearch({ searchKind: nextKind }); }, [handleSearch, locationPick, searchKind]); const loadDetails = useCallback(async () => { setLoadingDetails(true); try { // Load this round once; search filters run on the loaded rows. const response = mode === "approved" ? await getApproverInventoryLotDetailsAllApproved( selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, 0, 5000 ) : await getApproverInventoryLotDetailsAllPending( selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, 0, 5000 ); setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); setTotal(response.total ?? response.records?.length ?? 0); } catch (e) { console.error(e); setInventoryLotDetails([]); } finally { setLoadingDetails(false); } }, [selectedSession, mode]); useEffect(() => { loadDetails(); }, [loadDetails]); useEffect(() => { let cancelled = false; fetchWarehouseListClient() .then((rows) => { if (!cancelled) setLocationWarehouses(rows ?? []); }) .catch((e) => console.error("Failed to load warehouses for approver search:", e)); return () => { cancelled = true; }; }, []); const roundSectionCodeSet = useMemo(() => { const set = new Set(); inventoryLotDetails.forEach((row) => { const code = row.stockTakeSection?.trim(); if (code) set.add(code); }); return set; }, [inventoryLotDetails]); const roundWarehouses = useMemo( () => locationWarehouses.filter((row) => { const code = row.stockTakeSection?.trim(); if (!code) return false; if (roundSectionCodeSet.size === 0) return true; return roundSectionCodeSet.has(code); }), [locationWarehouses, roundSectionCodeSet] ); const sectionDescriptionOptions = useMemo(() => { const pickedCodes = new Set(selectedApproverSectionCodes(searchStockTakeSession)); const set = new Set(); roundWarehouses.forEach((row) => { const code = row.stockTakeSection?.trim(); if (pickedCodes.size > 0 && (!code || !pickedCodes.has(code))) return; const desc = row.stockTakeSectionDescription?.trim(); if (desc) set.add(desc); }); return Array.from(set).sort(compareApproverLabel); }, [roundWarehouses, searchStockTakeSession]); const stockTakeSectionOptions = useMemo(() => { const descFilter = searchSectionDescription !== "All" ? searchSectionDescription : ""; const set = new Set(); roundWarehouses.forEach((row) => { if (descFilter && (row.stockTakeSectionDescription?.trim() || "") !== descFilter) return; const code = row.stockTakeSection?.trim(); if (code) set.add(code); }); return Array.from(set).sort(compareApproverLabel); }, [roundWarehouses, searchSectionDescription]); const floorOptions = useMemo(() => { const set = new Set(); roundWarehouses.forEach((row) => { if (row.store_id) set.add(row.store_id); }); return withApproverAllOption(Array.from(set).sort(compareApproverLabel)); }, [roundWarehouses]); const warehouseOptions = useMemo(() => { if (!locationPick.storeId) return []; const set = new Set(); roundWarehouses.forEach((row) => { if (locationPick.storeId !== APPROVER_LOCATION_ALL && (row.store_id ?? "").trim() !== locationPick.storeId) return; const zone = row.warehouse?.trim(); if (zone) set.add(zone); }); return withApproverAllOption(Array.from(set).sort(compareApproverLabel)); }, [roundWarehouses, locationPick.storeId]); const areaOptions = useMemo(() => { if (!locationPick.storeId || !locationPick.warehouse) return []; const set = new Set(); roundWarehouses.forEach((row) => { if (locationPick.storeId !== APPROVER_LOCATION_ALL && (row.store_id ?? "").trim() !== locationPick.storeId) return; if (locationPick.warehouse !== APPROVER_LOCATION_ALL && (row.warehouse ?? "").trim() !== locationPick.warehouse) return; const area = row.area?.trim(); if (area) set.add(area); }); return withApproverAllOption(Array.from(set).sort(compareApproverLabel)); }, [roundWarehouses, locationPick.storeId, locationPick.warehouse]); const slotOptions = useMemo(() => { if (!locationPick.storeId || !locationPick.warehouse || !locationPick.area) return []; const set = new Set(); roundWarehouses.forEach((row) => { if (locationPick.storeId !== APPROVER_LOCATION_ALL && (row.store_id ?? "").trim() !== locationPick.storeId) return; if (locationPick.warehouse !== APPROVER_LOCATION_ALL && (row.warehouse ?? "").trim() !== locationPick.warehouse) return; if (locationPick.area !== APPROVER_LOCATION_ALL && (row.area ?? "").trim() !== locationPick.area) return; const slot = row.slot?.trim(); if (slot) set.add(slot); }); return withApproverAllOption(Array.from(set).sort(compareApproverLabel)); }, [roundWarehouses, locationPick.storeId, locationPick.warehouse, locationPick.area]); const locationWarehouseRows = useMemo( () => chunkApproverOptions(warehouseOptions, APPROVER_LOCATION_BUTTONS_PER_ROW), [warehouseOptions] ); const locationAreaRows = useMemo( () => chunkApproverOptions(areaOptions, APPROVER_LOCATION_BUTTONS_PER_ROW), [areaOptions] ); const locationSlotRows = useMemo( () => chunkApproverOptions(slotOptions, APPROVER_LOCATION_BUTTONS_PER_ROW), [slotOptions] ); const applyLocationPick = useCallback((pick: ApproverLocationPick) => { const specificWarehouse = Boolean(pick.warehouse && pick.warehouse !== APPROVER_LOCATION_ALL); const specificArea = Boolean(pick.area && pick.area !== APPROVER_LOCATION_ALL); const specificSlot = Boolean(pick.slot && pick.slot !== APPROVER_LOCATION_ALL); const matching = roundWarehouses.filter((row) => { if (pick.storeId && pick.storeId !== APPROVER_LOCATION_ALL && row.store_id !== pick.storeId) return false; if (specificWarehouse && row.warehouse !== pick.warehouse) return false; if (specificArea && row.area !== pick.area) return false; if (specificSlot && row.slot !== pick.slot) return false; return true; }); const codes = Array.from( new Set(matching.map((row) => row.stockTakeSection?.trim()).filter((code): code is string => Boolean(code))) ).sort(compareApproverLabel); const owned = specificWarehouse || specificArea || specificSlot ? codes.join(", ") : ""; locationOwnedSectionRef.current = owned; const keyword = approverWarehouseKeyword(pick); const storeId = !specificWarehouse && pick.storeId && pick.storeId !== APPROVER_LOCATION_ALL ? pick.storeId : "All"; setLocationPick(pick); handleSearch({ searchKind: "location", stockTakeSession: owned, warehouseKeyword: keyword, storeId, }); }, [roundWarehouses, handleSearch]); useEffect(() => { if (searchKind !== "location") return; if (!locationPick.storeId && !locationPick.warehouse && !locationPick.area && !locationPick.slot) return; if (locationWarehouses.length === 0) return; const floorOk = !locationPick.storeId || floorOptions.includes(locationPick.storeId); const warehouseOk = !locationPick.warehouse || warehouseOptions.includes(locationPick.warehouse); const areaOk = !locationPick.area || areaOptions.includes(locationPick.area); const slotOk = !locationPick.slot || slotOptions.includes(locationPick.slot); if (floorOk && warehouseOk && areaOk && slotOk) return; const next: ApproverLocationPick = { storeId: floorOk ? locationPick.storeId : "", warehouse: floorOk && warehouseOk ? locationPick.warehouse : "", area: floorOk && warehouseOk && areaOk ? locationPick.area : "", slot: floorOk && warehouseOk && areaOk && slotOk ? locationPick.slot : "", }; applyLocationPick(next); }, [applyLocationPick, areaOptions, floorOptions, locationPick, locationWarehouses.length, searchKind, slotOptions, warehouseOptions]); // 切换模式或盘次时,清空选择与搜索;approved 模式需要以后端结果为准。 useEffect(() => { setQtySelection({}); setApproverQty({}); setApproverBadQty({}); locationOwnedSectionRef.current = ""; setLocationPick({ storeId: "", warehouse: "", area: "", slot: "" }); setSearchSectionDescription("All"); setSearchStockTakeSession(""); setSearchItemKeyword(""); setSearchWarehouseKeyword(""); setSearchStoreId("All"); setSearchStatus(mode === "pending" ? "pass" : "All"); setSearchVariancePercentTolerance("5"); setSearchVarianceFilterInclusive(false); setSearchVarianceFilterStrict(false); setAppliedFilters(defaultApproverSearchFilters(mode)); setPage(0); }, [mode, selectedSession.stockTakeId]); useEffect(() => { const inferSelection = ( detail: InventoryLotDetailResponse ): QtySelectionType => { // 优先使用后端记录的 lastSelect(1=First, 2=Second, 3=Approver Input) if (detail.lastSelect != null) { if (detail.lastSelect === 1) return "first"; if (detail.lastSelect === 2) return "second"; if (detail.lastSelect === 3) return "approver"; } // 目标:在 approved 模式下,即使后端把 approver 字段也回填了, // 只要 finalQty 来自 first/second(picker 结果),就优先勾选 first/second。 // 只有匹配不到 first/second 时,才推断为 approver。 if (detail.finalQty != null) { const eps = 1e-6; const firstAvailable = detail.firstStockTakeQty; const secondAvailable = detail.secondStockTakeQty; // 如果这一行确实有 approver 结果,那么 approved 时应该优先显示为 approver // (尤其是:picker first 后又手动改 approver input 的情况) if (detail.approverQty != null) { const approverAvailable = detail.approverQty - (detail.approverBadQty ?? 0); if (Math.abs(approverAvailable - detail.finalQty) <= eps) { return "approver"; } } if (secondAvailable != null && Math.abs(secondAvailable - detail.finalQty) <= eps) { return "second"; } if (firstAvailable != null && Math.abs(firstAvailable - detail.finalQty) <= eps) { return "first"; } // approver 字段口径可能是「available」或「total+bad」两种之一,这里同时尝试两种。 if (detail.approverQty != null) { const approverAvailable = detail.approverQty; const approverAvailable2 = detail.approverQty - (detail.approverBadQty ?? 0); if ( Math.abs(approverAvailable - detail.finalQty) <= eps || Math.abs(approverAvailable2 - detail.finalQty) <= eps ) { return "approver"; } } } // pending/无法反推时:second 存在则默认 second,否则 first if (detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0) { return "second"; } return "first"; }; const newSelections: Record = {}; inventoryLotDetails.forEach((detail) => { if (qtySelection[detail.id]) return; newSelections[detail.id] = inferSelection(detail); }); if (Object.keys(newSelections).length > 0) { setQtySelection((prev) => ({ ...prev, ...newSelections })); } }, [inventoryLotDetails, qtySelection]); // approved 模式下:把已保存的 approver 输入值回填到 TextField,避免“radio 显示了但输入框为空” useEffect(() => { if (mode !== "approved") return; const newApproverQty: Record = {}; const newApproverBadQty: Record = {}; inventoryLotDetails.forEach((detail) => { if (detail.approverQty != null && approverQty[detail.id] == null) { newApproverQty[detail.id] = String(detail.approverQty); } if ( detail.approverBadQty != null && approverBadQty[detail.id] == null ) { newApproverBadQty[detail.id] = String(detail.approverBadQty); } }); if (Object.keys(newApproverQty).length > 0) { setApproverQty((prev) => ({ ...prev, ...newApproverQty })); } if (Object.keys(newApproverBadQty).length > 0) { setApproverBadQty((prev) => ({ ...prev, ...newApproverBadQty })); } }, [mode, inventoryLotDetails, approverQty, approverBadQty]); const calculateDifference = useCallback( (detail: InventoryLotDetailResponse, selection: QtySelectionType): number => { let selectedQty = 0; if (selection === "first") { selectedQty = detail.firstStockTakeQty || 0; } else if (selection === "second") { selectedQty = detail.secondStockTakeQty || 0; } else if (selection === "approver") { selectedQty = (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0; } const bookQty = detail.bookQty != null ? detail.bookQty : detail.availableQty || 0; return selectedQty - bookQty; }, [approverQty, approverBadQty] ); const filteredDetails = useMemo(() => { return inventoryLotDetails.filter((detail) => matchesApproverDetailFilters(detail, appliedFilters, mode)); }, [inventoryLotDetails, appliedFilters, mode]); const sortedDetails = useMemo(() => { const list = [...filteredDetails]; if (mode !== "approved") { return list.sort((a, b) => (a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, { numeric: true, sensitivity: "base", }) ); } const key = approvedSortKey ?? "stockTakeSection"; const mul = approvedSortDir === "asc" ? 1 : -1; return list.sort((a, b) => { let cmp = 0; switch (key) { case "stockTakeEndTime": cmp = parseDateTimeMs(a.approverTime ?? a.stockTakeEndTime) - parseDateTimeMs(b.approverTime ?? b.stockTakeEndTime); break; case "stockTakeSection": cmp = (a.stockTakeSection || "").localeCompare(b.stockTakeSection || "", undefined, { numeric: true, sensitivity: "base", }); break; case "item": cmp = `${a.itemCode || ""} ${a.itemName || ""}`.localeCompare( `${b.itemCode || ""} ${b.itemName || ""}`, undefined, { numeric: true, sensitivity: "base" } ); break; case "stockTakerName": cmp = (a.stockTakerName || "").localeCompare(b.stockTakerName || "", undefined, { numeric: true, sensitivity: "base", }); break; case "variance": cmp = Number(a.varianceQty ?? 0) - Number(b.varianceQty ?? 0); break; default: cmp = 0; } return cmp * mul; }); }, [filteredDetails, mode, approvedSortKey, approvedSortDir]); const handleApprovedSort = useCallback((property: ApprovedSortKey) => { if (approvedSortKey === property) { setApprovedSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setApprovedSortKey(property); setApprovedSortDir("asc"); } }, [approvedSortKey]); const handleSaveApproverStockTake = useCallback( async (detail: InventoryLotDetailResponse) => { if (mode === "approved") return; if (!selectedSession || !currentUserId) { return; } const built = buildApproverSaveRequest( detail, qtySelection, approverQty, approverBadQty, currentUserId, t ); if (!built.ok) { if (built.reason === "skip_approver_empty") { onSnackbar(t("Approver input empty; save skipped, row remains pending"), "warning"); return; } onSnackbar(built.message, "error"); return; } const { request, goodQty, finalQty, finalBadQty, selection } = built; setSaving(true); try { await saveApproverStockTakeRecord(request, selectedSession.stockTakeId); const gapText = selection === "approver" ? stockTakeQtyGapWarnText(t, approverQty[detail.id] || "", stockTakeHiddenOnHand(detail), qtyGapWarnPercent) : null; if (selection === "approver") { setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: true })); } onSnackbar( gapText ? `${t("Approver stock take record saved successfully")} ${gapText}` : t("Approver stock take record saved successfully"), gapText ? "warning" : "success", ); setInventoryLotDetails((prev) => prev.map((d) => d.id === detail.id ? { ...d, finalQty: goodQty, approverQty: selection === "approver" ? finalQty : d.approverQty, approverBadQty: selection === "approver" ? finalBadQty : d.approverBadQty, stockTakeRecordStatus: "completed", } : d ) ); } catch (e: any) { console.error("Save approver stock take record error:", e); let errorMessage = t("Failed to save approver stock take record"); if (e?.message) { errorMessage = e.message; } else if (e?.response) { try { const errorData = await e.response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { } } onSnackbar(errorMessage, "error"); } finally { setSaving(false); } }, [selectedSession, currentUserId, qtySelection, approverQty, approverBadQty, t, onSnackbar, mode, qtyGapWarnPercent] ); const handleUpdateStatusToNotMatch = useCallback( async (detail: InventoryLotDetailResponse) => { if (mode === "approved") return; if (!detail.stockTakeRecordId) { onSnackbar(t("Stock take record ID is required"), "error"); return; } setUpdatingStatus(true); try { await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); onSnackbar(t("Stock take record status updated to not match"), "success"); setInventoryLotDetails((prev) => prev.map((d) => d.id === detail.id ? { ...d, stockTakeRecordStatus: "notMatch" } : d ) ); } catch (e: any) { console.error("Update stock take record status error:", e); let errorMessage = t("Failed to update stock take record status"); if (e?.message) { errorMessage = e.message; } else if (e?.response) { try { const errorData = await e.response.json(); errorMessage = errorData.message || errorData.error || errorMessage; } catch { } } onSnackbar(errorMessage, "error"); } finally { setUpdatingStatus(false); } }, [t, onSnackbar] ); const blockNonIntegerKeys = (e: React.KeyboardEvent) => { // 禁止小数点、逗号、科学计数、正负号 if ([".", ",", "e", "E", "+", "-"].includes(e.key)) { e.preventDefault(); } }; const sanitizeIntegerInput = (value: string) => { // 只保留数字 return value.replace(/[^\d]/g, ""); }; const sanitizePositiveDecimalInput = (value: string) => { const unsigned = value.replace(/[^\d.]/g, ""); const parts = unsigned.split("."); return parts.length <= 1 ? unsigned : `${parts[0]}.${parts.slice(1).join("")}`; }; const isIntegerString = (value: string) => /^\d+$/.test(value); const batchApproverSaveRecords = useMemo( () => currentUserId ? buildBatchApproverSaveRequests( sortedDetails, qtySelection, approverQty, approverBadQty, currentUserId, t ) : [], [sortedDetails, qtySelection, approverQty, approverBadQty, currentUserId, t] ); const batchSaveSectionLabels = useMemo(() => { const labels = new Set(); sortedDetails.forEach((d) => { const sec = d.stockTakeSection?.trim(); if (sec) labels.add(sec); }); return Array.from(labels).sort((a, b) => a.localeCompare(b)); }, [sortedDetails]); const handleOpenBatchSaveConfirm = useCallback(() => { if (mode === "approved") return; if (!selectedSession || !currentUserId) return; if (inventoryLotDetails.length === 0) { onSnackbar(t("No rows loaded; set search criteria and search first"), "warning"); return; } if (batchApproverSaveRecords.length === 0) { onSnackbar(t("No valid records to batch save"), "warning"); return; } setOpenBatchSaveConfirmDialog(true); }, [ mode, selectedSession, currentUserId, inventoryLotDetails.length, batchApproverSaveRecords.length, onSnackbar, t, ]); const handleBatchSubmitAll = useCallback(async () => { if (batchSaveInFlightRef.current) return; if (mode === "approved") return; if (!selectedSession || !currentUserId) return; const records = buildBatchApproverSaveRequests( sortedDetails, qtySelection, approverQty, approverBadQty, currentUserId, t ); if (records.length === 0) { onSnackbar(t("No valid records to batch save"), "warning"); return; } batchSaveInFlightRef.current = true; setBatchSaving(true); setOpenBatchSaveConfirmDialog(false); try { const result = await batchSaveApproverStockTakeRecordsByIds({ stockTakeId: selectedSession.stockTakeId, approverId: currentUserId, recordIds: records .map((r) => r.stockTakeRecordId) .filter((id): id is number => typeof id === "number" && id > 0), records, }); onSnackbar( t("Batch approver save completed: {{success}} success, {{errors}} errors", { success: result.successCount, errors: result.errorCount, }), result.errorCount > 0 ? "warning" : "success", ); if (result.successCount > 0) { await loadDetails(); } } catch (e: unknown) { console.error("handleBatchSubmitAll (all): Error:", e); let errorMessage = t("Failed to batch save approver stock take records"); if (e instanceof Error && e.message) { errorMessage = e.message; } onSnackbar(errorMessage, "error"); } finally { setBatchSaving(false); batchSaveInFlightRef.current = false; } }, [ mode, selectedSession, currentUserId, sortedDetails, qtySelection, approverQty, approverBadQty, t, onSnackbar, loadDetails, ]); const formatNumber = (num: number | null | undefined): string => { if (num == null) return "0"; return num.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0, }); }; const uniqueWarehouses = useMemo( () => Array.from( new Set( inventoryLotDetails .map((detail) => detail.warehouse) .filter((warehouse) => warehouse && warehouse.trim() !== "") ) ).join(", "), [inventoryLotDetails] ); const formatRecordEndTime = (detail: InventoryLotDetailResponse) => { const ms = parseDateTimeMs(detail.approverTime ?? detail.stockTakeEndTime); if (!ms) return "-"; return dayjs(ms).format("YYYY-MM-DD HH:mm"); }; const rows = sortedDetails; const columns = useMemo[]>(() => { const cols: GridColDef[] = []; if (mode === "approved") { cols.push({ field: "approverTime", headerName: t("Approver Time"), minWidth: 150, flex: 0.6, sortable: false, renderCell: (params) => ( {formatRecordEndTime(params.row)} ), }); } cols.push( { field: "warehouseLocation", headerName: t("Warehouse Location"), minWidth: 220, flex: 1, sortable: false, renderCell: (params) => ( {params.row.stockTakeSection || "-"}{" "} {params.row.stockTakeSectionDescription || "-"} {params.row.warehouseCode || "-"} ), }, { field: "itemLot", headerName: t("Item-lotNo-ExpiryDate"), minWidth: 160, flex: 1, sortable: false, renderCell: (params) => ( {params.row.itemCode || "-"} {params.row.itemName || "-"} {params.row.lotNo || "-"} {params.row.expiryDate ? dayjs(params.row.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} ), }, { field: "uom", headerName: t("UOM"), minWidth: 90, flex: 0.4, sortable: false, valueGetter: (params) => params.row.uom || "-", }, { field: "qtyBlock", headerName: t("Stock Take Qty Data and Variance Analysis"), minWidth: 480, flex: 3.2, sortable: false, renderCell: (params: GridRenderCellParams) => { const detail = params.row; const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty >= 0; const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); const canApprover = mode === "pending" ? true : selection === "approver" && (detail.approverQty != null || detail.approverBadQty != null); const showRadioBlock = mode === "approved" || detail.finalQty == null; const bookQty = detail.bookQty ?? detail.availableQty ?? 0; const selectedQty = selection === "first" ? (detail.firstStockTakeQty ?? 0) : selection === "second" ? (detail.secondStockTakeQty ?? 0) : (parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0")) || 0; const difference = selectedQty - bookQty; const approverQtyNum = parseFloat(approverQty[detail.id] || "0") || 0; const approverBadQtyNum = parseFloat(approverBadQty[detail.id] || "0") || 0; const approverGoodQty = approverQtyNum - approverBadQtyNum; const variancePercentageRaw = bookQty !== 0 ? (difference / bookQty) * 100 : difference !== 0 ? difference > 0 ? 100 : -100 : 0; const variancePercentage = roundDownPercent(variancePercentageRaw); const hasVariance = difference !== 0; const pctLabel = `${variancePercentage}%`; const summaryLine = (label: string, value: string, valueColor?: string) => ( {label} {value} ); return ( {!showRadioBlock ? ( - ) : ( {hasFirst && ( setQtySelection({ ...qtySelection, [detail.id]: "first", }) } /> {t("First")}:{" "} )} {hasSecond && ( setQtySelection({ ...qtySelection, [detail.id]: "second", }) } /> {t("Second")}:{" "} )} {canApprover && ( setQtySelection({ ...qtySelection, [detail.id]: "approver", }) } /> {t("Approver Input")}: { setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: false })); if (mode !== "approved" && selection !== "approver") { setQtySelection({ ...qtySelection, [detail.id]: "approver", }); } }} onBlur={() => setGapCheckOpen((prev) => ({ ...prev, [`${detail.id}:approver`]: true })) } onKeyDown={blockNonIntegerKeys} onChange={(e) => { const clean = sanitizeIntegerInput(e.target.value); setApproverQty({ ...approverQty, [detail.id]: clean, }); }} InputProps={{ endAdornment: stockTakeQtyEndAdornment(detail.uomShortDesc), }} sx={{ width: 120, minWidth: 120, "& .MuiInputBase-input": { py: 0.5, px: 1, }, }} // placeholder={t("Stock Take Qty")} disabled={mode === "approved"} inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} /> {/* = {formatNumber(approverGoodQty)} */} )} {summaryLine( `${t("Selected Qty")}:`, [formatNumber(selectedQty), detail.uomShortDesc?.trim()].filter(Boolean).join(" ") )} {summaryLine(`${t("Book Qty")}:`, formatNumber(bookQty))} {summaryLine( `${t("Inventory Difference")}:`, formatNumber(difference), hasVariance ? "error.main" : "text.primary" )} {t("variance Percentage")}: {pctLabel} )} ); }, } ); {/* if (mode === "approved") { cols.push({ field: "varianceQty", headerName: t("Variance"), minWidth: 110, flex: 0.5, sortable: false, valueGetter: (params) => String(params.row.varianceQty ?? 0), }); } */} cols.push( /* { field: "remarks", headerName: t("Remark"), minWidth: 120, flex: 0.8, sortable: false, renderCell: (params) => ( {params.row.remarks || "-"} ), }, */ { field: "stockTakeRecordStatus", headerName: t("Record Status"), minWidth: 130, flex: 0.6, sortable: false, renderCell: (params) => { const raw = params.row.stockTakeRecordStatus; const status = String(raw ?? "").trim().toLowerCase(); const colorMap: Record = { completed: "success", notmatch: "warning", pending: "info", "not_match": "warning", }; return ( ); } }, { field: "stockTakerName", headerName: t("Picker"), minWidth: 140, flex: 0.7, sortable: false, valueGetter: (params) => params.row.stockTakerName || "-", }, { field: "actions", headerName: t("Action"), minWidth: 110, flex: 0.9, sortable: false, renderCell: (params) => { const detail = params.row; const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty >= 0; const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); return ( {mode === "pending" && ( <> )} {mode === "approved" && ( {selection === "first" ? t("First") : selection === "second" ? t("Second") : t("Approver Input")} )} ); }, } ); return cols; }, [ mode, t, qtySelection, approverQty, approverBadQty, saving, updatingStatus, blockNonIntegerKeys, sanitizeIntegerInput, handleSaveApproverStockTake, handleUpdateStatusToNotMatch, formatRecordEndTime, ]); const effectivePageSize = pageSize === "all" ? Math.max(total || 0, 0) : (pageSize as number); return ( {onBack && ( )} {uniqueWarehouses && ( <> {t("Warehouse")}: {uniqueWarehouses} )} {mode === "pending" && ( )} setShowFilters(expanded)} elevation={0} sx={{ mb: 2, border: "1px solid", borderColor: "divider", borderRadius: 1 }} > } sx={{ minHeight: 36, px: 2, "&.Mui-expanded": { minHeight: 36 }, "& .MuiAccordionSummary-content, & .MuiAccordionSummary-content.Mui-expanded": { my: 0.75 }, }} > {({t("Search Criteria")})} switchSearchKind(value)} sx={{ minHeight: 36 }} > {searchKind === "location" && ( {t("Store ID")} { applyLocationPick({ storeId: next ?? "", warehouse: "", area: "", slot: "" }); }} > {floorOptions.map((floor) => ( {floor === APPROVER_LOCATION_ALL ? t("All") : floor} ))} {t("Warehouse")} {locationWarehouseRows.map((row, rowIndex) => ( { if (next == null) return; applyLocationPick({ ...locationPick, warehouse: next, area: "", slot: "" }); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: APPROVER_LOCATION_BUTTON_WIDTH, minWidth: APPROVER_LOCATION_BUTTON_WIDTH, maxWidth: APPROVER_LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((zone) => ( {zone === APPROVER_LOCATION_ALL ? t("All") : zone} ))} ))} {!locationPick.storeId ? ( {t("Select floor first")} ) : null} {t("Area")} {locationAreaRows.map((row, rowIndex) => ( { if (next == null) return; applyLocationPick({ ...locationPick, area: next, slot: "" }); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: APPROVER_LOCATION_BUTTON_WIDTH, minWidth: APPROVER_LOCATION_BUTTON_WIDTH, maxWidth: APPROVER_LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((area) => ( {area === APPROVER_LOCATION_ALL ? t("All") : area} ))} ))} {!locationPick.warehouse ? ( {locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} ) : null} {t("Slot")} {locationSlotRows.map((row, rowIndex) => ( { if (next == null) return; applyLocationPick({ ...locationPick, slot: next }); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: APPROVER_LOCATION_BUTTON_WIDTH, minWidth: APPROVER_LOCATION_BUTTON_WIDTH, maxWidth: APPROVER_LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((slot) => ( {slot === APPROVER_LOCATION_ALL ? t("All") : slot} ))} ))} {!locationPick.area ? ( {locationPick.warehouse ? t("Select area first") : locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} ) : null} )} {searchKind === "section" && ( {t("Stock Take Section")} )} {searchKind === "section" && ( {t("Stock Take Section code")} )} setSearchItemKeyword(e.target.value)} onBlur={() => handleSearch({ itemKeyword: searchItemKeyword })} onKeyDown={(e) => { if (e.key === "Enter") handleSearch({ itemKeyword: searchItemKeyword }); }} /> {mode === "pending" && ( {t("Record Status")} )} { const next = sanitizePositiveDecimalInput(e.target.value); setSearchVariancePercentTolerance(next); handleSearch({ variancePercentTolerance: next }); }} inputProps={{ min: 0, step: 0.1 }} sx={approverSearchFieldSx} /> { setSearchVarianceFilterInclusive(e.target.checked); handleSearch({ varianceFilterInclusive: e.target.checked }); }} /> } label={ {t("Variance filter inclusive only")} } /> {searchVarianceFilterInclusive ? t("Variance filter inclusive range hint", { value: searchVariancePercentTolerance || "0", op: searchVarianceFilterStrict ? "<" : "≤", }) : t("Variance filter exclusive range hint", { value: searchVariancePercentTolerance || "0", op: searchVarianceFilterStrict ? ">" : "≥", })} {mode !== "pending" && ( )} {t("Total")}: {total} | {t("Shown")}: {sortedDetails.length} row.id} loading={loadingDetails} disableRowSelectionOnClick paginationMode="client" // ← important: client-side pageSizeOptions={[50, 100, 200, 500]} getRowHeight={() => "auto"} sx={{ backgroundColor: "transparent", "& .MuiDataGrid-columnHeaders": { backgroundColor: "#fff" }, "& .MuiDataGrid-cell": { py: 1, alignItems: "flex-start" }, "& .MuiDataGrid-row": { minHeight: 96 }, }} /> { if (!batchSaving) setOpenBatchSaveConfirmDialog(false); }} maxWidth="sm" fullWidth > {t("Confirm batch save approver")} {t("Batch save confirm message", { count: batchApproverSaveRecords.length })} {batchSaveSectionLabels.length > 0 ? ( {t("Stock take sections in current list", { count: batchSaveSectionLabels.length })} {batchSaveSectionLabels.map((section) => ( {section} ))} ) : null} ); }; export default ApproverStockTakeAll;