From e7b5a608588735f2f1a30ae97de0a60caa941eb4 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 13 Jan 2026 10:49:58 +0800 Subject: [PATCH] update stock record --- src/app/(main)/stockRecord/page.tsx | 25 + src/app/api/jo/actions.ts | 58 +- src/app/api/stockTake/actions.ts | 106 ++- src/components/Jodetail/JodetailSearch.tsx | 3 + .../Jodetail/MaterialPickStatusTable.tsx | 381 +++++++++++ .../ProductionProcess/JobProcessStatus.tsx | 329 +++++++++ .../ProductionProcessPage.tsx | 5 + src/components/StockRecord/SearchPage.tsx | 444 ++++++++++++ src/components/StockRecord/index.tsx | 26 + .../StockTakeManagement/ApproverCardList.tsx | 18 +- .../StockTakeManagement/ApproverStockTake.tsx | 634 +++++++++++------- .../StockTakeManagement/PickerCardList.tsx | 18 +- .../StockTakeManagement/PickerReStockTake.tsx | 482 ++++++------- .../StockTakeManagement/PickerStockTake.tsx | 625 +++++++++-------- 14 files changed, 2338 insertions(+), 816 deletions(-) create mode 100644 src/app/(main)/stockRecord/page.tsx create mode 100644 src/components/Jodetail/MaterialPickStatusTable.tsx create mode 100644 src/components/ProductionProcess/JobProcessStatus.tsx create mode 100644 src/components/StockRecord/SearchPage.tsx create mode 100644 src/components/StockRecord/index.tsx diff --git a/src/app/(main)/stockRecord/page.tsx b/src/app/(main)/stockRecord/page.tsx new file mode 100644 index 0000000..d144167 --- /dev/null +++ b/src/app/(main)/stockRecord/page.tsx @@ -0,0 +1,25 @@ +import SearchPage from "@/components/StockRecord/index"; +import { getServerI18n } from "@/i18n"; +import { I18nProvider } from "@/i18n"; +import { Metadata } from "next"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Stock Record", +}; + +const SearchView: React.FC = async () => { + const { t } = await getServerI18n("inventory"); + + return ( + <> + + }> + + + + + ); +}; + +export default SearchView; \ No newline at end of file diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 2b76c7c..aa3eb2d 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -724,6 +724,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async () => { } ); }); + /* export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => { return serverFetchJson( @@ -1167,4 +1168,59 @@ export const updateProductProcessLineProcessingTimeSetupTimeChangeoverTime = asy headers: { "Content-Type": "application/json" }, } ); -}; \ No newline at end of file + +}; +export interface MaterialPickStatusItem { + id: number; + pickOrderId: number | null; + pickOrderCode: string | null; + jobOrderId: number | null; + jobOrderCode: string | null; + itemId: number | null; + itemCode: string | null; + itemName: string | null; + jobOrderQty: number | null; + uom: string | null; + pickStartTime: string | null; // ISO datetime string + pickEndTime: string | null; // ISO datetime string + numberOfItemsToPick: number; + numberOfItemsWithIssue: number; + pickStatus: string | null; +} + +export const fetchMaterialPickStatus = cache(async (): Promise => { + return await serverFetchJson( + `${BASE_API_URL}/jo/material-pick-status`, + { + method: "GET", + } + ); +}) +export interface ProcessStatusInfo { + startTime?: string | null; + endTime?: string | null; + equipmentCode?: string | null; + isRequired: boolean; +} + +export interface JobProcessStatusResponse { + jobOrderId: number; + jobOrderCode: string; + itemCode: string; + itemName: string; + planEndTime?: string | null; + processes: ProcessStatusInfo[]; +} + +// 添加API调用函数 +export const fetchJobProcessStatus = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/product-process/Demo/JobProcessStatus`, + { + method: "GET", + next: { tags: ["jobProcessStatus"] }, + } + ); +}); + +; \ No newline at end of file diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index c092195..e54376a 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -3,6 +3,11 @@ import { cache } from 'react'; import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson import { BASE_API_URL } from "@/config/api"; + +export interface RecordsRes { + records: T[]; + total: number; +} export interface InventoryLotDetailResponse { id: number; inventoryLotId: number; @@ -39,30 +44,34 @@ export interface InventoryLotDetailResponse { export const getInventoryLotDetailsBySection = async ( stockTakeSection: string, - stockTakeId?: number | null + stockTakeId?: number | null, + pageNum?: number, + pageSize?: number ) => { console.log('🌐 [API] getInventoryLotDetailsBySection called with:', { stockTakeSection, - stockTakeId + stockTakeId, + pageNum, + pageSize }); const encodedSection = encodeURIComponent(stockTakeSection); - let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}`; + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}&pageNum=${pageNum}&pageSize=${pageSize}`; if (stockTakeId != null && stockTakeId > 0) { url += `&stockTakeId=${stockTakeId}`; } console.log(' [API] Full URL:', url); - const details = await serverFetchJson( + const response = await serverFetchJson>( url, { method: "GET", }, ); - console.log('[API] Response received:', details); - return details; + console.log('[API] Response received:', response); + return response; } export interface SaveStockTakeRecordRequest { stockTakeRecordId?: number | null; @@ -100,6 +109,7 @@ export const importStockTake = async (data: FormData) => { } export const getStockTakeRecords = async () => { + const stockTakeRecords = await serverFetchJson( // 改为 serverFetchJson `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList`, { @@ -277,28 +287,86 @@ export const updateStockTakeRecordStatusToNotMatch = async ( export const getInventoryLotDetailsBySectionNotMatch = async ( stockTakeSection: string, - stockTakeId?: number | null + stockTakeId?: number | null, + pageNum: number = 0, + pageSize: number = 10 ) => { - console.log('🌐 [API] getInventoryLotDetailsBySectionNotMatch called with:', { - stockTakeSection, - stockTakeId - }); - const encodedSection = encodeURIComponent(stockTakeSection); - let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}`; + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}&pageNum=${pageNum}`; + + // Only add pageSize if it's not "all" (which would be a large number) + if (pageSize < 100000) { + url += `&pageSize=${pageSize}`; + } + // If pageSize is large (meaning "all"), don't send it - backend will return all + if (stockTakeId != null && stockTakeId > 0) { url += `&stockTakeId=${stockTakeId}`; } - console.log(' [API] Full URL:', url); - - const details = await serverFetchJson( + const response = await serverFetchJson>( url, { method: "GET", }, ); - - console.log('[API] Response received:', details); - return details; + return response; } + +export interface SearchStockTransactionRequest { + startDate: string | null; + endDate: string | null; + itemCode: string | null; + itemName: string | null; + type: string | null; + pageNum: number; + pageSize: number; +} +export interface StockTransactionResponse { + id: number; + transactionType: string; + itemId: number; + itemCode: string | null; + itemName: string | null; + balanceQty: number | null; + qty: number; + type: string | null; + status: string; + transactionDate: string | null; + date: string | null; // 添加这个字段 + lotNo: string | null; + stockInId: number | null; + stockOutId: number | null; + remarks: string | null; +} + +export interface StockTransactionListResponse { + records: RecordsRes; +} + +export const searchStockTransactions = cache(async (request: SearchStockTransactionRequest) => { + // 构建查询字符串 + const params = new URLSearchParams(); + + if (request.itemCode) params.append("itemCode", request.itemCode); + if (request.itemName) params.append("itemName", request.itemName); + if (request.type) params.append("type", request.type); + if (request.startDate) params.append("startDate", request.startDate); + if (request.endDate) params.append("endDate", request.endDate); + params.append("pageNum", String(request.pageNum || 0)); + params.append("pageSize", String(request.pageSize || 100)); + + const queryString = params.toString(); + const url = `${BASE_API_URL}/stockTakeRecord/searchStockTransactions${queryString ? `?${queryString}` : ''}`; + + const response = await serverFetchJson>( + url, + { + method: "GET", + next: { tags: ["Stock Transaction List"] }, + } + ); + // 确保返回正确的格式 + return response?.records || []; +}); + diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index 22165c8..81f5b1e 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -37,6 +37,7 @@ import { import { fetchPrinterCombo } from "@/app/api/settings/printer"; import { PrinterCombo } from "@/app/api/settings/printer"; import JoPickOrderDetail from "./JoPickOrderDetail"; +import MaterialPickStatusTable from "./MaterialPickStatusTable"; interface Props { pickOrders: PickOrderResult[]; printerCombo: PrinterCombo[]; @@ -489,6 +490,7 @@ const JodetailSearch: React.FC = ({ pickOrders, printerCombo }) => { + @@ -503,6 +505,7 @@ const JodetailSearch: React.FC = ({ pickOrders, printerCombo }) => { printQty={printQty} /> )} + {tabIndex === 2 && } ); diff --git a/src/components/Jodetail/MaterialPickStatusTable.tsx b/src/components/Jodetail/MaterialPickStatusTable.tsx new file mode 100644 index 0000000..4246138 --- /dev/null +++ b/src/components/Jodetail/MaterialPickStatusTable.tsx @@ -0,0 +1,381 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, + TablePagination, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import { arrayToDayjs } from '@/app/utils/formatUtil'; +import { fetchMaterialPickStatus, MaterialPickStatusItem } from '@/app/api/jo/actions'; + +const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds + +const MaterialPickStatusTable: React.FC = () => { + const { t } = useTranslation("jo"); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const refreshCountRef = useRef(0); + const [paginationController, setPaginationController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + const loadData = useCallback(async () => { + setLoading(true); + try { + const result = await fetchMaterialPickStatus(); + // On second refresh, clear completed pick orders + if (refreshCountRef.current >= 1) { + // const filtered = result.filter(item => + // item.pickStatus?.toLowerCase() !== 'completed' + //); + setData(result); + } else { + setData(result || []); + } + refreshCountRef.current += 1; + } catch (error) { + console.error('Error fetching material pick status:', error); + setData([]); // Set empty array on error to stop loading + } finally { + setLoading(false); + } + }, []); // Remove refreshCount from dependencies + + useEffect(() => { + // Initial load + loadData(); + + // Set up auto-refresh every 10 minutes + const interval = setInterval(() => { + loadData(); + }, REFRESH_INTERVAL); + + return () => clearInterval(interval); + }, [loadData]); // Only depend on loadData, which is now stable + + const formatTime = (timeData: any): string => { + if (!timeData) return ''; + + // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54") + if (typeof timeData === 'string') { + // Try parsing as ISO string first (most common format from LocalDateTime) + const parsed = dayjs(timeData); + if (parsed.isValid()) { + return parsed.format('HH:mm'); + } + + // Try parsing as custom format YYYYMMDDHHmmss + const customParsed = dayjs(timeData, 'YYYYMMDDHHmmss'); + if (customParsed.isValid()) { + return customParsed.format('HH:mm'); + } + + // Try parsing as time string (HH:mm or HH:mm:ss) + const parts = timeData.split(':'); + if (parts.length >= 2) { + const hour = parseInt(parts[0], 10); + const minute = parseInt(parts[1] || '0', 10); + if (!isNaN(hour) && !isNaN(minute)) { + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } + } + } else if (Array.isArray(timeData)) { + // Handle array format [year, month, day, hour, minute, second] + const hour = timeData[3] ?? timeData[0] ?? 0; + const minute = timeData[4] ?? timeData[1] ?? 0; + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } + + return ''; + }; + + const calculatePickTime = (startTime: any, endTime: any): number => { + if (!startTime || !endTime) return 0; + + let start: dayjs.Dayjs; + let end: dayjs.Dayjs; + + // Parse start time + if (Array.isArray(startTime)) { + // Array format: [year, month, day, hour, minute, second] + if (startTime.length >= 5) { + const year = startTime[0] || 0; + const month = (startTime[1] || 1) - 1; // month is 0-indexed in JS Date + const day = startTime[2] || 1; + const hour = startTime[3] || 0; + const minute = startTime[4] || 0; + const second = startTime[5] || 0; + + // Create Date object and convert to dayjs + const date = new Date(year, month, day, hour, minute, second); + start = dayjs(date); + console.log('Parsed start time:', { + array: startTime, + date: date.toISOString(), + dayjs: start.format('YYYY-MM-DD HH:mm:ss'), + isValid: start.isValid() + }); + } else { + // Fallback to arrayToDayjs for shorter arrays + start = arrayToDayjs(startTime, true); + } + } else if (typeof startTime === 'string') { + // Try ISO format first + start = dayjs(startTime); + if (!start.isValid()) { + // Try custom format + start = dayjs(startTime, 'YYYYMMDDHHmmss'); + } + } else { + start = dayjs(startTime); + } + + // Parse end time + if (Array.isArray(endTime)) { + // Array format: [year, month, day, hour, minute, second] + if (endTime.length >= 5) { + const year = endTime[0] || 0; + const month = (endTime[1] || 1) - 1; // month is 0-indexed in JS Date + const day = endTime[2] || 1; + const hour = endTime[3] || 0; + const minute = endTime[4] || 0; + const second = endTime[5] || 0; + + // Create Date object and convert to dayjs + const date = new Date(year, month, day, hour, minute, second); + end = dayjs(date); + console.log('Parsed end time:', { + array: endTime, + date: date.toISOString(), + dayjs: end.format('YYYY-MM-DD HH:mm:ss'), + isValid: end.isValid() + }); + } else { + // Fallback to arrayToDayjs for shorter arrays + end = arrayToDayjs(endTime, true); + } + } else if (typeof endTime === 'string') { + // Try ISO format first + end = dayjs(endTime); + if (!end.isValid()) { + // Try custom format + end = dayjs(endTime, 'YYYYMMDDHHmmss'); + } + } else { + end = dayjs(endTime); + } + + if (!start.isValid() || !end.isValid()) { + console.warn('Invalid time values:', { + startTime, + endTime, + startValid: start.isValid(), + endValid: end.isValid(), + startFormat: start.isValid() ? start.format() : 'invalid', + endFormat: end.isValid() ? end.format() : 'invalid' + }); + return 0; + } + + // Calculate difference in seconds first, then convert to minutes + // This handles sub-minute differences correctly + const diffSeconds = end.diff(start, 'second'); + const diffMinutes = Math.ceil(diffSeconds / 60); // Round up to nearest minute + + console.log('Time calculation:', { + start: start.format('YYYY-MM-DD HH:mm:ss'), + end: end.format('YYYY-MM-DD HH:mm:ss'), + diffSeconds, + diffMinutes + }); + + return diffMinutes > 0 ? diffMinutes : 0; + }; + + const handlePageChange = useCallback((event: unknown, newPage: number) => { + setPaginationController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setPaginationController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); + + const paginatedData = useMemo(() => { + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return data.slice(startIndex, endIndex); + }, [data, paginationController]); + + return ( + + + {/* Title */} + + + {t("Material Pick Status")} + + + + + + + + {loading ? ( + + + + ) : ( + <> + + + + + + + + {t("Pick Order No.- Job Order No.- Item")} + + + + + + + + + {t("Job Order Qty")} + + + + + + + + + {t("No. of Items to be Picked")} + + + + + + + + {t("No. of Items with Issue During Pick")} + + + + + + + + {t("Pick Start Time")} + + + + + + + + {t("Pick End Time")} + + + + + + + + {t("Pick Time Taken (minutes)")} + + + + + + + + {paginatedData.length === 0 ? ( + + + {t("No data available")} + + + ) : ( + paginatedData.map((row) => { + const pickTimeTaken = calculatePickTime(row.pickStartTime, row.pickEndTime); + + return ( + + + {row.pickOrderCode || '-'} +
+ {row.jobOrderCode || '-'} +
+ {row.itemCode || '-'} {row.itemName || '-'} + + +
+ + + {row.jobOrderQty !== null && row.jobOrderQty !== undefined + ? `${row.jobOrderQty} ${row.uom || ''}` + : '-'} + + {row.numberOfItemsToPick ?? 0} + {row.numberOfItemsWithIssue ?? 0} + {formatTime(row.pickStartTime) || '-'} + {formatTime(row.pickEndTime) || '-'} + + {pickTimeTaken > 0 ? `${pickTimeTaken} ${t("minutes")}` : '-'} + +
+ ); + }) + )} +
+
+
+ {data.length > 0 && ( + + )} + + )} +
+
+
+ ); +}; + +export default MaterialPickStatusTable; \ No newline at end of file diff --git a/src/components/ProductionProcess/JobProcessStatus.tsx b/src/components/ProductionProcess/JobProcessStatus.tsx new file mode 100644 index 0000000..085174b --- /dev/null +++ b/src/components/ProductionProcess/JobProcessStatus.tsx @@ -0,0 +1,329 @@ +"use client"; + +import React, { useState, useEffect, useCallback, useRef } from 'react'; + +import { + Box, + Typography, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import { fetchJobProcessStatus, JobProcessStatusResponse } from '@/app/api/jo/actions'; +import { arrayToDayjs } from '@/app/utils/formatUtil'; + +const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes + +const JobProcessStatus: React.FC = () => { + const { t } = useTranslation(["common", "jo"]); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const refreshCountRef = useRef(0); + const [currentTime, setCurrentTime] = useState(dayjs()); + + // Update current time every second for countdown + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(dayjs()); + }, 1000); + return () => clearInterval(timer); + }, []); + + const loadData = useCallback(async () => { + setLoading(true); + try { + const result = await fetchJobProcessStatus(); + + // On second refresh, filter out completed jobs + if (refreshCountRef.current >= 1) { + const filtered = result.filter(item => { + // Check if all required processes are completed + const allCompleted = item.processes + .filter(p => p.isRequired) + .every(p => p.endTime != null); + return !allCompleted; + }); + setData(filtered); + } else { + setData(result); + } + refreshCountRef.current += 1; + } catch (error) { + console.error('Error fetching job process status:', error); + setData([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadData(); + const interval = setInterval(() => { + loadData(); + }, REFRESH_INTERVAL); + return () => clearInterval(interval); + }, [loadData]); + + const formatTime = (timeData: any): string => { + if (!timeData) return '-'; // 改为返回 '-' 而不是 'N/A' + + // Handle array format [year, month, day, hour, minute, second] + if (Array.isArray(timeData)) { + try { + const parsed = arrayToDayjs(timeData, true); + if (parsed.isValid()) { + return parsed.format('HH:mm'); + } + } catch (error) { + console.error('Error parsing array time:', error); + } + } + + // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54") + if (typeof timeData === 'string') { + const parsed = dayjs(timeData); + if (parsed.isValid()) { + return parsed.format('HH:mm'); + } + } + + return '-'; + }; + + const calculateRemainingTime = (planEndTime: any): string => { + if (!planEndTime) return '-'; + + let endTime: dayjs.Dayjs; + + // Handle array format [year, month, day, hour, minute, second] + // 使用与 OverallTimeRemainingCard 相同的方式处理 + if (Array.isArray(planEndTime)) { + try { + const [year, month, day, hour = 0, minute = 0, second = 0] = planEndTime; + // 注意:JavaScript Date 构造函数中月份是 0-based,所以需要 month - 1 + endTime = dayjs(new Date(year, month - 1, day, hour, minute, second)); + console.log('Parsed planEndTime array:', { + array: planEndTime, + parsed: endTime.format('YYYY-MM-DD HH:mm:ss'), + isValid: endTime.isValid() + }); + } catch (error) { + console.error('Error parsing array planEndTime:', error); + return '-'; + } + } else if (typeof planEndTime === 'string') { + endTime = dayjs(planEndTime); + console.log('Parsed planEndTime string:', { + string: planEndTime, + parsed: endTime.format('YYYY-MM-DD HH:mm:ss'), + isValid: endTime.isValid() + }); + } else { + return '-'; + } + + if (!endTime.isValid()) { + console.error('Invalid endTime:', planEndTime); + return '-'; + } + + const diff = endTime.diff(currentTime, 'minute'); + console.log('Remaining time calculation:', { + endTime: endTime.format('YYYY-MM-DD HH:mm:ss'), + currentTime: currentTime.format('YYYY-MM-DD HH:mm:ss'), + diffMinutes: diff + }); + + if (diff < 0) return '0'; + + const hours = Math.floor(diff / 60); + const minutes = diff % 60; + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + }; + + const calculateWaitTime = ( + currentProcessEndTime: any, + nextProcessStartTime: any, + isLastProcess: boolean + ): string => { + if (isLastProcess) return '-'; + if (!currentProcessEndTime) return '-'; + if (nextProcessStartTime) return '0'; // Next process has started, stop counting + + let endTime: dayjs.Dayjs; + + // Handle array format + if (Array.isArray(currentProcessEndTime)) { + try { + endTime = arrayToDayjs(currentProcessEndTime, true); + } catch (error) { + console.error('Error parsing array endTime:', error); + return '-'; + } + } else if (typeof currentProcessEndTime === 'string') { + endTime = dayjs(currentProcessEndTime); + } else { + return '-'; + } + + if (!endTime.isValid()) return '-'; + + const diff = currentTime.diff(endTime, 'minute'); + return diff > 0 ? diff.toString() : '0'; + }; + + return ( + + + + + {t("Job Process Status", { ns: "jobProcessStatus" })} + + + + + + {loading ? ( + + + + ) : ( + + + + + + + {t("Job Order No.", { ns: "jobProcessStatus" })} + + + + + {t("FG / WIP Item", { ns: "jobProcessStatus" })} + + + + + {t("Production Time Remaining", { ns: "jobProcessStatus" })} + + + + + {t("Process Status / Time [hh:mm]", { ns: "jobProcessStatus" })} + + + + + {[1, 2, 3, 4, 5, 6].map((num) => ( + + + {t("Process", { ns: "jobProcessStatus" })} {num} + + + ))} + + + {[1, 2, 3, 4, 5, 6].map((num) => ( + + + + {t("Start", { ns: "jobProcessStatus" })} + + + {t("Finish", { ns: "jobProcessStatus" })} + + + {t("Wait Time [minutes]", { ns: "jobProcessStatus" })} + + + + ))} + + + + {data.length === 0 ? ( + + + {t("No data available")} + + + ) : ( + data.map((row) => ( + + + {row.jobOrderCode || '-'} + + + {row.itemCode || '-'} + {row.itemName || '-'} + + + + {calculateRemainingTime(row.planEndTime)} + + {row.processes.map((process, index) => { + const isLastProcess = index === row.processes.length - 1 || + !row.processes.slice(index + 1).some(p => p.isRequired); + const nextProcess = index < row.processes.length - 1 ? row.processes[index + 1] : null; + const waitTime = calculateWaitTime( + process.endTime, + nextProcess?.startTime, + isLastProcess + ); + + // 如果工序不是必需的,只显示一个 N/A + if (!process.isRequired) { + return ( + + + N/A + + + ); + } + + // 如果工序是必需的,显示三行(Start、Finish、Wait Time) + return ( + + + {process.equipmentCode || '-'} + + {formatTime(process.startTime)} + + + {formatTime(process.endTime)} + + 0 ? 'warning.main' : 'text.primary' + }}> + {waitTime} + + + + ); + })} + + )) + )} + +
+
+ )} +
+ + +
+
+ ); +}; + +export default JobProcessStatus; \ No newline at end of file diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index bdad5e6..3297d79 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -8,6 +8,7 @@ import ProductionProcessDetail from "@/components/ProductionProcess/ProductionPr import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; import FinishedQcJobOrderList from "@/components/ProductionProcess/FinishedQcJobOrderList"; +import JobProcessStatus from "@/components/ProductionProcess/JobProcessStatus"; import { fetchProductProcesses, fetchProductProcessesByJobOrderId, @@ -164,6 +165,7 @@ const ProductionProcessPage: React.FC = ({ printerCo + {tabIndex === 0 && ( @@ -190,6 +192,9 @@ const ProductionProcessPage: React.FC = ({ printerCo selectedPrinter={selectedPrinter} /> )} + {tabIndex === 2 && ( + + )} ); }; diff --git a/src/components/StockRecord/SearchPage.tsx b/src/components/StockRecord/SearchPage.tsx new file mode 100644 index 0000000..8d1c02a --- /dev/null +++ b/src/components/StockRecord/SearchPage.tsx @@ -0,0 +1,444 @@ +"use client"; + +import SearchBox, { Criterion } from "../SearchBox"; +import { useCallback, useMemo, useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import { StockTransactionResponse, SearchStockTransactionRequest } from "@/app/api/stockTake/actions"; +import { decimalFormatter } from "@/app/utils/formatUtil"; +import { Stack, Box } from "@mui/material"; +import { searchStockTransactions } from "@/app/api/stockTake/actions"; + +interface Props { + dataList: StockTransactionResponse[]; +} + +type SearchQuery = { + itemCode?: string; + itemName?: string; + type?: string; + startDate?: string; + endDate?: string; +}; + +// 扩展类型以包含计算字段 +interface ExtendedStockTransaction extends StockTransactionResponse { + formattedDate: string; + inQty: number; + outQty: number; + balanceQty: number; +} + +const SearchPage: React.FC = ({ dataList: initialDataList }) => { + const { t } = useTranslation("inventory"); + + // 添加数据状态 + const [dataList, setDataList] = useState(initialDataList); + const [loading, setLoading] = useState(false); + const [filterArgs, setFilterArgs] = useState>({}); + const isInitialMount = useRef(true); + + // 添加分页状态 + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [pagingController, setPagingController] = useState({ pageNum: 1, pageSize: 10 }); + const [hasSearchQuery, setHasSearchQuery] = useState(false); + const [totalCount, setTotalCount] = useState(initialDataList.length); + + const processedData = useMemo(() => { + // 按日期和 itemId 排序 - 优先使用 date 字段,如果没有则使用 transactionDate + const sorted = [...dataList].sort((a, b) => { + // 优先使用 date 字段,如果没有则使用 transactionDate 的日期部分 + const getDateValue = (item: StockTransactionResponse): number => { + if (item.date) { + return new Date(item.date).getTime(); + } + if (item.transactionDate) { + if (Array.isArray(item.transactionDate)) { + const [year, month, day] = item.transactionDate; + return new Date(year, month - 1, day).getTime(); + } else { + return new Date(item.transactionDate).getTime(); + } + } + return 0; + }; + + const dateA = getDateValue(a); + const dateB = getDateValue(b); + + if (dateA !== dateB) return dateA - dateB; // 从旧到新排序 + return a.itemId - b.itemId; + }); + + // 计算每个 item 的累计余额 + const balanceMap = new Map(); // itemId -> balance + const processed: ExtendedStockTransaction[] = []; + + sorted.forEach((item) => { + const currentBalance = balanceMap.get(item.itemId) || 0; + let newBalance = currentBalance; + + // 根据类型计算余额 + if (item.transactionType === "IN") { + newBalance = currentBalance + item.qty; + } else if (item.transactionType === "OUT") { + newBalance = currentBalance - item.qty; + } + + balanceMap.set(item.itemId, newBalance); + + // 格式化日期 - 优先使用 date 字段 + let formattedDate = ""; + if (item.date) { + // 如果 date 是字符串格式 "yyyy-MM-dd" + const date = new Date(item.date); + if (!isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + formattedDate = `${year}-${month}-${day}`; + } + } else if (item.transactionDate) { + // 回退到 transactionDate + if (Array.isArray(item.transactionDate)) { + const [year, month, day] = item.transactionDate; + formattedDate = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + } else if (typeof item.transactionDate === 'string') { + const date = new Date(item.transactionDate); + if (!isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + formattedDate = `${year}-${month}-${day}`; + } + } else { + const date = new Date(item.transactionDate); + if (!isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + formattedDate = `${year}-${month}-${day}`; + } + } + } + + processed.push({ + ...item, + formattedDate, + inQty: item.transactionType === "IN" ? item.qty : 0, + outQty: item.transactionType === "OUT" ? item.qty : 0, + balanceQty: item.balanceQty ? item.balanceQty : newBalance, + }); + }); + + return processed; + }, [dataList]); + // 修复:使用 processedData 初始化 filteredList + const [filteredList, setFilteredList] = useState(processedData); + + // 当 processedData 变化时更新 filteredList(不更新 pagingController,避免循环) + useEffect(() => { + setFilteredList(processedData); + setTotalCount(processedData.length); + // 只在初始加载时设置 pageSize + if (isInitialMount.current && processedData.length > 0) { + setPageSize("all"); + setPagingController(prev => ({ ...prev, pageSize: processedData.length })); + setPage(0); + isInitialMount.current = false; + } + }, [processedData]); + + // API 调用函数(参考 PoSearch 的实现) + // API 调用函数(参考 PoSearch 的实现) +const newPageFetch = useCallback( + async ( + pagingController: Record, + filterArgs: Record, + ) => { + setLoading(true); + try { + // 处理空字符串,转换为 null + const itemCode = filterArgs.itemCode?.trim() || null; + const itemName = filterArgs.itemName?.trim() || null; + + // 验证:至少需要 itemCode 或 itemName + if (!itemCode && !itemName) { + console.warn("Search requires at least itemCode or itemName"); + setDataList([]); + setTotalCount(0); + return; + } + + const params: SearchStockTransactionRequest = { + itemCode: itemCode, + itemName: itemName, + type: filterArgs.type?.trim() || null, + startDate: filterArgs.startDate || null, + endDate: filterArgs.endDate || null, + pageNum: pagingController.pageNum - 1 || 0, + pageSize: pagingController.pageSize || 100, + }; + + console.log("Search params:", params); // 添加调试日志 + + const res = await searchStockTransactions(params); + console.log("Search response:", res); // 添加调试日志 + + if (res && Array.isArray(res)) { + setDataList(res); + } else { + console.error("Invalid response format:", res); + setDataList([]); + } + } catch (error) { + console.error("Fetch error:", error); + setDataList([]); + } finally { + setLoading(false); + } + }, + [], +); + + // 使用 useRef 来存储上一次的值,避免不必要的 API 调用 + const prevPagingControllerRef = useRef(pagingController); + const prevFilterArgsRef = useRef(filterArgs); + const hasSearchedRef = useRef(false); + // 当 filterArgs 或 pagingController 变化时调用 API(只在真正变化时调用) + useEffect(() => { + // 检查是否有有效的搜索条件 + const hasValidSearch = filterArgs.itemCode || filterArgs.itemName; + + if (!hasValidSearch) { + // 如果没有有效搜索条件,只更新 ref,不调用 API + if (isInitialMount.current) { + isInitialMount.current = false; + } + prevFilterArgsRef.current = filterArgs; + return; + } + + // 检查是否真的变化了 + const pagingChanged = + prevPagingControllerRef.current.pageNum !== pagingController.pageNum || + prevPagingControllerRef.current.pageSize !== pagingController.pageSize; + + const filterChanged = JSON.stringify(prevFilterArgsRef.current) !== JSON.stringify(filterArgs); + + // 如果是第一次有效搜索,或者条件/分页发生变化,则调用 API + if (!hasSearchedRef.current || pagingChanged || filterChanged) { + newPageFetch(pagingController, filterArgs); + prevPagingControllerRef.current = pagingController; + prevFilterArgsRef.current = filterArgs; + hasSearchedRef.current = true; + isInitialMount.current = false; + } + }, [newPageFetch, pagingController, filterArgs]); + + // 分页处理函数 + const handleChangePage = useCallback((event: unknown, newPage: number) => { + setPage(newPage); + setPagingController(prev => ({ ...prev, pageNum: newPage + 1 })); + }, []); + + const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent) => { + const newSize = parseInt(event.target.value, 10); + if (newSize === -1) { + setPageSize("all"); + setPagingController(prev => ({ ...prev, pageSize: filteredList.length, pageNum: 1 })); + } else if (!isNaN(newSize)) { + setPageSize(newSize); + setPagingController(prev => ({ ...prev, pageSize: newSize, pageNum: 1 })); + } + setPage(0); + }, [filteredList.length]); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Item Code"), + paramName: "itemCode", + type: "text", + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { + label: t("Type"), + paramName: "type", + type: "text", + }, + { + label: t("Start Date"), + paramName: "startDate", + type: "date", + }, + { + label: t("End Date"), + paramName: "endDate", + type: "date", + }, + ], + [t], + ); + + const columns = useMemo[]>( + () => [ + { + name: "formattedDate" as keyof ExtendedStockTransaction, + label: t("Date"), + align: "left", + }, + { + name: "itemCode" as keyof ExtendedStockTransaction, + label: t("Item-lotNo"), + align: "left", + renderCell: (item) => ( + + + {item.itemCode || "-"} {item.itemName || "-"} + {item.lotNo || "-"} + + + ), + }, + { + name: "inQty" as keyof ExtendedStockTransaction, + label: t("In Qty"), + align: "left", + type: "decimal", + renderCell: (item) => ( + <>{item.inQty > 0 ? decimalFormatter.format(item.inQty) : ""} + ), + }, + { + name: "outQty" as keyof ExtendedStockTransaction, + label: t("Out Qty"), + align: "left", + type: "decimal", + renderCell: (item) => ( + <>{item.outQty > 0 ? decimalFormatter.format(item.outQty) : ""} + ), + }, + { + name: "balanceQty" as keyof ExtendedStockTransaction, + label: t("Balance Qty"), + align: "left", + type: "decimal", + }, + { + name: "type", + label: t("Type"), + align: "left", + renderCell: (item) => { + if (!item.type) return "-"; + return t(item.type.toLowerCase()); + }, + }, + { + name: "status", + label: t("Status"), + align: "left", + renderCell: (item) => { + if (!item.status) return "-"; + return t(item.status.toLowerCase()); + }, + }, + ], + [t], + ); + + const handleSearch = useCallback((query: Record) => { + // 检查是否有搜索条件 + const itemCode = query.itemCode?.trim(); + const itemName = query.itemName?.trim(); + const type = query.type?.trim(); + const startDate = query.startDate === "Invalid Date" ? "" : query.startDate; + const endDate = query.endDate === "Invalid Date" ? "" : query.endDate; + + // 验证:至少需要 itemCode 或 itemName + if (!itemCode && !itemName) { + // 可以显示提示信息 + console.warn("Please enter at least Item Code or Item Name"); + return; + } + + const hasQuery = !!(itemCode || itemName || type || startDate || endDate); + setHasSearchQuery(hasQuery); + + // 更新 filterArgs,触发 useEffect 调用 API + setFilterArgs({ + itemCode: itemCode || undefined, + itemName: itemName || undefined, + type: type || undefined, + startDate: startDate || undefined, + endDate: endDate || undefined, + }); + + // 重置分页 + setPage(0); + setPagingController(prev => ({ ...prev, pageNum: 1 })); + }, []); + + const handleReset = useCallback(() => { + setHasSearchQuery(false); + // 重置 filterArgs,触发 useEffect 调用 API + setFilterArgs({}); + setPage(0); + setPagingController(prev => ({ ...prev, pageNum: 1 })); + }, []); + + // 计算实际显示的 items(分页) + const paginatedItems = useMemo(() => { + if (pageSize === "all") { + return filteredList; + } + const actualPageSize = typeof pageSize === 'number' ? pageSize : 10; + const startIndex = page * actualPageSize; + const endIndex = startIndex + actualPageSize; + return filteredList.slice(startIndex, endIndex); + }, [filteredList, page, pageSize]); + + // 计算传递给 SearchResults 的 pageSize(确保在选项中) + const actualPageSizeForTable = useMemo(() => { + if (pageSize === "all") { + return filteredList.length; + } + const size = typeof pageSize === 'number' ? pageSize : 10; + // 如果 size 不在标准选项中,使用 "all" 模式 + if (![10, 25, 100].includes(size)) { + return filteredList.length; + } + return size; + }, [pageSize, filteredList.length]); + + return ( + <> + + {loading && {t("Loading...")}} + + items={paginatedItems} + columns={columns} + pagingController={{ ...pagingController, pageSize: actualPageSizeForTable }} + setPagingController={setPagingController} + totalCount={totalCount} + isAutoPaging={false} + /> + + ); +}; + +export default SearchPage; \ No newline at end of file diff --git a/src/components/StockRecord/index.tsx b/src/components/StockRecord/index.tsx new file mode 100644 index 0000000..e5b59a6 --- /dev/null +++ b/src/components/StockRecord/index.tsx @@ -0,0 +1,26 @@ +import GeneralLoading from "../General/GeneralLoading"; +import SearchPage from "./SearchPage"; +import { searchStockTransactions } from "@/app/api/stockTake/actions"; + +interface SubComponents { + Loading: typeof GeneralLoading; +} + +const Wrapper: React.FC & SubComponents = async () => { + // 初始加载时使用空参数,SearchPage 会在用户搜索时调用 API + const dataList = await searchStockTransactions({ + startDate: null, + endDate: null, + itemCode: null, + itemName: null, + type: null, + pageNum: 0, + pageSize: 100, + }); + + return ; +}; + +Wrapper.Loading = GeneralLoading; + +export default Wrapper; \ No newline at end of file diff --git a/src/components/StockTakeManagement/ApproverCardList.tsx b/src/components/StockTakeManagement/ApproverCardList.tsx index 153f5a7..8c92cdf 100644 --- a/src/components/StockTakeManagement/ApproverCardList.tsx +++ b/src/components/StockTakeManagement/ApproverCardList.tsx @@ -201,23 +201,7 @@ const ApproverCardList: React.FC = ({ onCardClick }) => { {t("Control Time")}: - {session.totalInventoryLotNumber > 0 && ( - - - - {t("Progress")} - - - {completionRate}% - - - - - )} + diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx index a036bd0..512898a 100644 --- a/src/components/StockTakeManagement/ApproverStockTake.tsx +++ b/src/components/StockTakeManagement/ApproverStockTake.tsx @@ -14,10 +14,14 @@ import { TableHead, TableRow, Paper, + Checkbox, TextField, + FormControlLabel, Radio, + TablePagination, + ToggleButton } from "@mui/material"; -import { useState, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { AllPickedStockTakeListReponse, @@ -52,7 +56,8 @@ const ApproverStockTake: React.FC = ({ const [inventoryLotDetails, setInventoryLotDetails] = useState([]); const [loadingDetails, setLoadingDetails] = useState(false); - + const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false); + // 每个记录的选择状态,key 为 detail.id const [qtySelection, setQtySelection] = useState>({}); const [approverQty, setApproverQty] = useState>({}); @@ -60,28 +65,111 @@ const ApproverStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [updatingStatus, setUpdatingStatus] = useState(false); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState("all"); + const [total, setTotal] = useState(0); + const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); - useEffect(() => { - const loadDetails = async () => { - setLoadingDetails(true); - try { - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - } finally { - setLoadingDetails(false); + const handleChangePage = useCallback((event: 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 loadDetails = useCallback(async (pageNum: number, size: number | string) => { + setLoadingDetails(true); + try { + let actualSize: number; + if (size === "all") { + if (selectedSession.totalInventoryLotNumber > 0) { + actualSize = selectedSession.totalInventoryLotNumber; + } else if (total > 0) { + actualSize = total; + } else { + actualSize = 10000; + } + } else { + actualSize = typeof size === 'string' ? parseInt(size, 10) : size; } - }; - loadDetails(); - }, [selectedSession]); + + const response = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, + pageNum, + actualSize + ); + setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); + setTotal(response.total || 0); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + setTotal(0); + } finally { + setLoadingDetails(false); + } + }, [selectedSession, total]); + useEffect(() => { + loadDetails(page, pageSize); + }, [page, pageSize, loadDetails]); + 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.availableQty || 0; + return selectedQty - bookQty; + }, [approverQty, approverBadQty]); + + // 3. 修改默认选择逻辑(在 loadDetails 的 useEffect 中,或创建一个新的 useEffect) + useEffect(() => { + // 初始化默认选择:如果 second 存在则选择 second,否则选择 first + const newSelections: Record = {}; + inventoryLotDetails.forEach(detail => { + if (!qtySelection[detail.id]) { + // 如果 second 不为 null 且大于 0,默认选择 second,否则选择 first + if (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0) { + newSelections[detail.id] = "second"; + } else { + newSelections[detail.id] = "first"; + } + } + }); + + if (Object.keys(newSelections).length > 0) { + setQtySelection(prev => ({ ...prev, ...newSelections })); + } + }, [inventoryLotDetails]); + + // 4. 添加过滤逻辑(在渲染表格之前) + const filteredDetails = useMemo(() => { + if (!showOnlyWithDifference) { + return inventoryLotDetails; + } + + return inventoryLotDetails.filter(detail => { + const selection = qtySelection[detail.id] || (detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0 ? "second" : "first"); + const difference = calculateDifference(detail, selection); + return difference !== 0; + }); + }, [inventoryLotDetails, showOnlyWithDifference, qtySelection, calculateDifference]); + const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { if (!selectedSession || !currentUserId) { return; @@ -135,11 +223,7 @@ const ApproverStockTake: React.FC = ({ onSnackbar(t("Approver stock take record saved successfully"), "success"); - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + await loadDetails(page, pageSize); } catch (e: any) { console.error("Save approver stock take record error:", e); let errorMessage = t("Failed to save approver stock take record"); @@ -159,7 +243,8 @@ const ApproverStockTake: React.FC = ({ } finally { setSaving(false); } - }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar]); + }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); + const handleUpdateStatusToNotMatch = useCallback(async (detail: InventoryLotDetailResponse) => { if (!detail.stockTakeRecordId) { onSnackbar(t("Stock take record ID is required"), "error"); @@ -171,12 +256,6 @@ const ApproverStockTake: React.FC = ({ await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); onSnackbar(t("Stock take record status updated to not match"), "success"); - - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); } catch (e: any) { console.error("Update stock take record status error:", e); let errorMessage = t("Failed to update stock take record status"); @@ -195,8 +274,20 @@ const ApproverStockTake: React.FC = ({ onSnackbar(errorMessage, "error"); } finally { setUpdatingStatus(false); + // Reload after status update - the useEffect will handle it with current page/pageSize + // Or explicitly reload: + setPage((currentPage) => { + setPageSize((currentPageSize) => { + setTimeout(() => { + loadDetails(currentPage, currentPageSize); + }, 0); + return currentPageSize; + }); + return currentPage; + }); } - }, [selectedSession, t, onSnackbar]); + }, [selectedSession, t, onSnackbar, loadDetails]); + const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); @@ -223,11 +314,7 @@ const ApproverStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + await loadDetails(page, pageSize); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save approver stock take records"); @@ -247,11 +334,12 @@ const ApproverStockTake: React.FC = ({ } finally { setBatchSaving(false); } - }, [selectedSession, t, currentUserId, onSnackbar]); + }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; }, [handleBatchSubmitAll]); + const formatNumber = (num: number | null | undefined): string => { if (num == null) return "0.00"; return num.toLocaleString('en-US', { @@ -259,6 +347,7 @@ const ApproverStockTake: React.FC = ({ maximumFractionDigits: 2 }); }; + const uniqueWarehouses = Array.from( new Set( inventoryLotDetails @@ -266,6 +355,7 @@ const ApproverStockTake: React.FC = ({ .filter(warehouse => warehouse && warehouse.trim() !== "") ) ).join(", "); + const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { // Only allow editing if there's a first stock take qty if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { @@ -280,232 +370,270 @@ const ApproverStockTake: React.FC = ({ {t("Back to List")} - - {t("Stock Take Section")}: {selectedSession.stockTakeSession} - {uniqueWarehouses && ( - <> {t("Warehouse")}: {uniqueWarehouses} - )} - + + {t("Stock Take Section")}: {selectedSession.stockTakeSession} + {uniqueWarehouses && ( + <> {t("Warehouse")}: {uniqueWarehouses} + )} + - - + + + + + {loadingDetails ? ( ) : ( - - - - - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - {t("Stock Take Qty(include Bad Qty)= Available Qty")} - {t("Remark")} - {t("UOM")} - {t("Record Status")} - {t("Action")} - - - - {inventoryLotDetails.length === 0 ? ( + <> + +
+ - - - {t("No data")} - - + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} - ) : ( - inventoryLotDetails.map((detail) => { - const submitDisabled = isSubmitDisabled(detail); - const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty > 0; - const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0; - const selection = qtySelection[detail.id] || "first"; + + + {filteredDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + filteredDetails.map((detail) => { + const submitDisabled = isSubmitDisabled(detail); + const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty > 0; + const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0; + const selection = qtySelection[detail.id] || (hasSecond ? "second" : "first"); - return ( - - {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} - - - {detail.itemCode || "-"} {detail.itemName || "-"} - {detail.lotNo || "-"} - {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} - {/**/} - - - - - {detail.finalQty != null ? ( - + return ( + + {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} + - - {t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber((detail.finalQty || 0) - (detail.availableQty || 0))} - + {detail.itemCode || "-"} {detail.itemName || "-"} + {detail.lotNo || "-"} + {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} - ) : ( - - - {hasFirst && ( - - setQtySelection({ ...qtySelection, [detail.id]: "first" })} - /> - - {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} - - - )} - - - {hasSecond && ( - - setQtySelection({ ...qtySelection, [detail.id]: "second" })} - /> - - {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} - - - )} - - - {hasSecond && ( - - setQtySelection({ ...qtySelection, [detail.id]: "approver" })} - /> - {t("Approver Input")}: - setApproverQty({ ...approverQty, [detail.id]: e.target.value })} - sx={{ - width: 130, - minWidth: 130, - '& .MuiInputBase-input': { - height: '1.4375em', - - padding: '4px 8px' - } - }} - placeholder={t("Stock Take Qty") } - disabled={selection !== "approver"} - /> - - setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} - sx={{ - width: 130, - minWidth: 130, - '& .MuiInputBase-input': { - height: '1.4375em', - padding: '4px 8px' - } - }} - placeholder={t("Bad Qty")} - disabled={selection !== "approver"} - /> - - ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} - - - )} - - - {(() => { - 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.availableQty || 0; - const difference = selectedQty - bookQty; - - return ( - - {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} - - ); - })()} - - )} - - - - - {detail.remarks || "-"} - - - - {detail.uom || "-"} - - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - - )} - - - {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( - - - - )} -
- {detail.finalQty == null && ( - - - - )} -
-
- ); - }) - )} -
-
-
+ + + + {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)} + + ); + })()} + + ) : ( + + {hasFirst && ( + + setQtySelection({ ...qtySelection, [detail.id]: "first" })} + /> + + {t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)} + + + )} + + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "second" })} + /> + + {t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)} + + + )} + + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "approver" })} + /> + {t("Approver Input")}: + setApproverQty({ ...approverQty, [detail.id]: e.target.value })} + sx={{ + width: 130, + minWidth: 130, + '& .MuiInputBase-input': { + height: '1.4375em', + padding: '4px 8px' + } + }} + placeholder={t("Stock Take Qty") } + disabled={selection !== "approver"} + /> + + setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} + sx={{ + width: 130, + minWidth: 130, + '& .MuiInputBase-input': { + height: '1.4375em', + padding: '4px 8px' + } + }} + placeholder={t("Bad Qty")} + disabled={selection !== "approver"} + /> + + ={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))} + + + )} + + {(() => { + 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.availableQty || 0; + const difference = selectedQty - bookQty; + const differenceColor = difference > 0 + ? 'error.main' + : difference < 0 + ? 'error.main' + : 'success.main'; + + return ( + + {t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)} + + ); + })()} + + )} + + + + + {detail.remarks || "-"} + + + + {detail.uom || "-"} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( + + + + )} +
+ {detail.finalQty == null && ( + + + + )} +
+ + ); + }) + )} + + + + + )} ); diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx index a6affe8..15c437a 100644 --- a/src/components/StockTakeManagement/PickerCardList.tsx +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -224,23 +224,7 @@ const PickerCardList: React.FC = ({ onCardClick, onReStockT {t("Control Time")}: {t("Total Item Number")}: {session.totalItemNumber} - {session.totalInventoryLotNumber > 0 && ( - - - - {t("Progress")} - - - {completionRate}% - - - - - )} + diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx index e47dbe8..e186194 100644 --- a/src/components/StockTakeManagement/PickerReStockTake.tsx +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -15,6 +15,7 @@ import { TableRow, Paper, TextField, + TablePagination, } from "@mui/material"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; @@ -33,13 +34,13 @@ import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; -interface PickerStockTakeProps { +interface PickerReStockTakeProps { selectedSession: AllPickedStockTakeListReponse; onBack: () => void; onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; } -const PickerStockTake: React.FC = ({ +const PickerReStockTake: React.FC = ({ selectedSession, onBack, onSnackbar, @@ -60,28 +61,63 @@ const PickerStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState("all"); + const [total, setTotal] = useState(0); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); + + const handleChangePage = useCallback((event: 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); + }, []); - useEffect(() => { - const loadDetails = async () => { - setLoadingDetails(true); - try { - const details = await getInventoryLotDetailsBySectionNotMatch( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - } finally { - setLoadingDetails(false); + const loadDetails = useCallback(async (pageNum: number, size: number | string) => { + setLoadingDetails(true); + try { + let actualSize: number; + if (size === "all") { + if (selectedSession.totalInventoryLotNumber > 0) { + actualSize = selectedSession.totalInventoryLotNumber; + } else if (total > 0) { + actualSize = total; + } else { + actualSize = 10000; + } + } else { + actualSize = typeof size === 'string' ? parseInt(size, 10) : size; } - }; - loadDetails(); - }, [selectedSession]); + + const response = await getInventoryLotDetailsBySectionNotMatch( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, + pageNum, + actualSize + ); + setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); + setTotal(response.total || 0); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + setTotal(0); + } finally { + setLoadingDetails(false); + } + }, [selectedSession, total]); + + useEffect(() => { + loadDetails(page, pageSize); + }, [page, pageSize, loadDetails]); const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { setEditingRecord(detail); @@ -131,9 +167,9 @@ const PickerStockTake: React.FC = ({ badQty: parseFloat(badQty), remark: isSecondSubmit ? (remark || null) : null, }; - console.log('handleSaveStockTake: request:', request); - console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); - console.log('handleSaveStockTake: currentUserId:', currentUserId); + console.log('handleSaveStockTake: request:', request); + console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); + console.log('handleSaveStockTake: currentUserId:', currentUserId); await saveStockTakeRecord( request, selectedSession.stockTakeId, @@ -143,11 +179,7 @@ const PickerStockTake: React.FC = ({ onSnackbar(t("Stock take record saved successfully"), "success"); handleCancelEdit(); - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + await loadDetails(page, pageSize); } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); @@ -167,7 +199,7 @@ const PickerStockTake: React.FC = ({ } finally { setSaving(false); } - }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]); + }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); const handleBatchSubmitAll = useCallback(async () => { if (!selectedSession || !currentUserId) { @@ -195,11 +227,7 @@ const PickerStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + await loadDetails(page, pageSize); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save stock take records"); @@ -219,7 +247,7 @@ const PickerStockTake: React.FC = ({ } finally { setBatchSaving(false); } - }, [selectedSession, t, currentUserId, onSnackbar]); + }, [selectedSession, t, currentUserId, onSnackbar, page, pageSize, loadDetails]); useEffect(() => { handleBatchSubmitAllRef.current = handleBatchSubmitAll; @@ -325,213 +353,213 @@ const PickerStockTake: React.FC = ({ ) : ( - - - - - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - - {t("Qty")} - {t("Bad Qty")} - {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} - {t("Remark")} - - {t("UOM")} - - {t("Record Status")} - {t("Action")} - - - - {inventoryLotDetails.length === 0 ? ( + <> + +
+ - - - {t("No data")} - - + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + {t("Qty")} + {t("Bad Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} - ) : ( - inventoryLotDetails.map((detail) => { - const isEditing = editingRecord?.id === detail.id; - const submitDisabled = isSubmitDisabled(detail); - const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; - const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + + + {inventoryLotDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; - return ( - - {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} - - - {detail.itemCode || "-"} {detail.itemName || "-"} - {detail.lotNo || "-"} - {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} - {/**/} - - - - - - {isEditing && isFirstSubmit ? ( - setFirstQty(e.target.value)} - sx={{ width: 100 }} - - /> - ) : detail.firstStockTakeQty ? ( - - {t("First")}: {detail.firstStockTakeQty.toFixed(2)} - - ) : null} - + return ( + + {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} + + + {detail.itemCode || "-"} {detail.itemName || "-"} + {detail.lotNo || "-"} + {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} + + + + + + {isEditing && isFirstSubmit ? ( + setFirstQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.firstStockTakeQty ? ( + + {t("First")}: {detail.firstStockTakeQty.toFixed(2)} + + ) : null} + + {isEditing && isSecondSubmit ? ( + setSecondQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.secondStockTakeQty ? ( + + {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} + + ) : null} + + {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( + + - + + )} + + + + + {isEditing && isFirstSubmit ? ( + setFirstBadQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( + + {t("First")}: {detail.firstBadQty.toFixed(2)} + + ) : ( + + {t("First")}: 0.00 + + )} + + {isEditing && isSecondSubmit ? ( + setSecondBadQty(e.target.value)} + sx={{ width: 100 }} + /> + ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( + + {t("Second")}: {detail.secondBadQty.toFixed(2)} + + ) : null} + + {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( + + - + + )} + + + {isEditing && isSecondSubmit ? ( - setSecondQty(e.target.value)} - sx={{ width: 100 }} - - /> - ) : detail.secondStockTakeQty ? ( + <> + {t("Remark")} + setRemark(e.target.value)} + sx={{ width: 150 }} + /> + + ) : ( - {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} - - ) : null} - - {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( - - - + {detail.remarks || "-"} )} - - - - - {isEditing && isFirstSubmit ? ( - setFirstBadQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.firstBadQty != null && detail.firstBadQty > 0 ? ( - - {t("First")}: {detail.firstBadQty.toFixed(2)} - - ) : ( - - - {t("First")}: 0.00 - - )} - - {isEditing && isSecondSubmit ? ( - setSecondBadQty(e.target.value)} - sx={{ width: 100 }} - /> - ) : detail.secondBadQty != null && detail.secondBadQty > 0 ? ( - - {t("Second")}: {detail.secondBadQty.toFixed(2)} - - ) : null} - - {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( - - - - - )} - - - - {isEditing && isSecondSubmit ? ( - <> - {t("Remark")} - setRemark(e.target.value)} - sx={{ width: 150 }} - // If you want a single-line input, remove multiline/rows: - // multiline - // rows={2} - /> - - ) : ( - - {detail.remarks || "-"} - - )} - - {detail.uom || "-"} + + {detail.uom || "-"} - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - - )} - - - {isEditing ? ( - - + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {isEditing ? ( + + + + + ) : ( - - - ) : ( - - )} - - - ); - }) - )} - -
-
+ )} + + + ); + }) + )} + + + + + )} ); }; -export default PickerStockTake; \ No newline at end of file +export default PickerReStockTake; \ No newline at end of file diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx index e1dfa1b..9c49f44 100644 --- a/src/components/StockTakeManagement/PickerStockTake.tsx +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -15,7 +15,13 @@ import { TableRow, Paper, TextField, + TablePagination, + Select, // Add this + MenuItem, // Add this + FormControl, // Add this + InputLabel, } from "@mui/material"; +import { SelectChangeEvent } from "@mui/material/Select"; import { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { @@ -60,29 +66,76 @@ const PickerStockTake: React.FC = ({ const [saving, setSaving] = useState(false); const [batchSaving, setBatchSaving] = useState(false); const [shortcutInput, setShortcutInput] = useState(""); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState("all"); + + const [total, setTotal] = useState(0); + const totalPages = pageSize === "all" ? 1 : Math.ceil(total / (pageSize as number)); const currentUserId = session?.id ? parseInt(session.id) : undefined; const handleBatchSubmitAllRef = useRef<() => Promise>(); - - useEffect(() => { - const loadDetails = async () => { - setLoadingDetails(true); - try { - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); - } catch (e) { - console.error(e); - setInventoryLotDetails([]); - } finally { - setLoadingDetails(false); + const handleChangePage = useCallback((event: unknown, newPage: number) => { + setPage(newPage); + }, []); + const handlePageSelectChange = useCallback((event: SelectChangeEvent) => { + const newPage = parseInt(event.target.value as string, 10) - 1; // Convert to 0-indexed + setPage(Math.max(0, Math.min(newPage, totalPages - 1))); + }, [totalPages]); + + 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 loadDetails = useCallback(async (pageNum: number, size: number | string) => { + console.log('loadDetails called with:', { pageNum, size, selectedSessionTotal: selectedSession.totalInventoryLotNumber }); + setLoadingDetails(true); + try { + let actualSize: number; + if (size === "all") { + // Use totalInventoryLotNumber from selectedSession if available + if (selectedSession.totalInventoryLotNumber > 0) { + actualSize = selectedSession.totalInventoryLotNumber; + console.log('Using "all" - actualSize set to totalInventoryLotNumber:', actualSize); + } else if (total > 0) { + // Fallback to total from previous response + actualSize = total; + console.log('Using "all" - actualSize set to total from state:', actualSize); + } else { + // Last resort: use a large number + actualSize = 10000; + console.log('Using "all" - actualSize set to default 10000'); + } + } else { + actualSize = typeof size === 'string' ? parseInt(size, 10) : size; + console.log('Using specific size - actualSize set to:', actualSize); } - }; - loadDetails(); - }, [selectedSession]); - + + console.log('Calling getInventoryLotDetailsBySection with actualSize:', actualSize); + const response = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null, + pageNum, + actualSize + ); + setInventoryLotDetails(Array.isArray(response.records) ? response.records : []); + setTotal(response.total || 0); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + setTotal(0); + } finally { + setLoadingDetails(false); + } + }, [selectedSession, total]); + + useEffect(() => { + loadDetails(page, pageSize); + }, [page, pageSize, loadDetails]); const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { setEditingRecord(detail); @@ -176,12 +229,9 @@ const PickerStockTake: React.FC = ({ onSnackbar(t("Stock take record saved successfully"), "success"); handleCancelEdit(); - - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + + await loadDetails(page, pageSize); + } catch (e: any) { console.error("Save stock take record error:", e); let errorMessage = t("Failed to save stock take record"); @@ -213,6 +263,9 @@ const PickerStockTake: React.FC = ({ t, currentUserId, onSnackbar, + loadDetails, + page, + pageSize, ] ); @@ -243,11 +296,7 @@ const PickerStockTake: React.FC = ({ result.errorCount > 0 ? "warning" : "success" ); - const details = await getInventoryLotDetailsBySection( - selectedSession.stockTakeSession, - selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null - ); - setInventoryLotDetails(Array.isArray(details) ? details : []); + await loadDetails(page, pageSize); } catch (e: any) { console.error("handleBatchSubmitAll: Error:", e); let errorMessage = t("Failed to batch save stock take records"); @@ -393,278 +442,290 @@ const PickerStockTake: React.FC = ({ ) : ( - - - - - {t("Warehouse Location")} - {t("Item-lotNo-ExpiryDate")} - {t("Stock Take Qty(include Bad Qty)= Available Qty")} - {t("Remark")} - {t("UOM")} - {t("Record Status")} - {t("Action")} - - - - {inventoryLotDetails.length === 0 ? ( + <> + +
+ - - - {t("No data")} - - + {t("Warehouse Location")} + {t("Item-lotNo-ExpiryDate")} + {t("Stock Take Qty(include Bad Qty)= Available Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} - ) : ( - inventoryLotDetails.map((detail) => { - const isEditing = editingRecord?.id === detail.id; - const submitDisabled = isSubmitDisabled(detail); - const isFirstSubmit = - !detail.stockTakeRecordId || !detail.firstStockTakeQty; - const isSecondSubmit = - detail.stockTakeRecordId && - detail.firstStockTakeQty && - !detail.secondStockTakeQty; - - return ( - - - {detail.warehouseArea || "-"} - {detail.warehouseSlot || "-"} - - - - - {detail.itemCode || "-"} {detail.itemName || "-"} - - {detail.lotNo || "-"} - - {detail.expiryDate - ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) - : "-"} - - - - - {/* Qty + Bad Qty 合并显示/输入 */} - - - {/* First */} - {isEditing && isFirstSubmit ? ( - - {t("First")}: - setFirstQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Stock Take Qty")} - /> - setFirstBadQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Bad Qty")} - /> + + + {inventoryLotDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = + !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = + detail.stockTakeRecordId && + detail.firstStockTakeQty && + !detail.secondStockTakeQty; + + return ( + + + {detail.warehouseArea || "-"} + {detail.warehouseSlot || "-"} + + + + + {detail.itemCode || "-"} {detail.itemName || "-"} + + {detail.lotNo || "-"} + + {detail.expiryDate + ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) + : "-"} + + + + + {/* Qty + Bad Qty 合并显示/输入 */} + + + {/* First */} + {isEditing && isFirstSubmit ? ( + + {t("First")}: + setFirstQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Stock Take Qty")} + /> + setFirstBadQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Bad Qty")} + /> + + = + {formatNumber( + parseFloat(firstQty || "0") - + parseFloat(firstBadQty || "0") + )} + + + ) : detail.firstStockTakeQty != null ? ( - = + {t("First")}:{" "} + {formatNumber( + (detail.firstStockTakeQty ?? 0) + + (detail.firstBadQty ?? 0) + )}{" "} + ( {formatNumber( - parseFloat(firstQty || "0") - - parseFloat(firstBadQty || "0") + detail.firstBadQty ?? 0 )} + ) ={" "} + {formatNumber(detail.firstStockTakeQty ?? 0)} - - ) : detail.firstStockTakeQty != null ? ( - - {t("First")}:{" "} - {formatNumber( - (detail.firstStockTakeQty ?? 0) + - (detail.firstBadQty ?? 0) - )}{" "} - ( - {formatNumber( - detail.firstBadQty ?? 0 + ) : null} + + {/* Second */} + {isEditing && isSecondSubmit ? ( + + {t("Second")}: + setSecondQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Stock Take Qty")} + /> + setSecondBadQty(e.target.value)} + sx={{ + width: 130, + minWidth: 130, + "& .MuiInputBase-input": { + height: "1.4375em", + padding: "4px 8px", + }, + }} + placeholder={t("Bad Qty")} + /> + + = + {formatNumber( + parseFloat(secondQty || "0") - + parseFloat(secondBadQty || "0") + )} + + + ) : detail.secondStockTakeQty != null ? ( + + {t("Second")}:{" "} + {formatNumber( + (detail.secondStockTakeQty ?? 0) + + (detail.secondBadQty ?? 0) + )}{" "} + ( + {formatNumber( + detail.secondBadQty ?? 0 + )} + ) ={" "} + {formatNumber(detail.secondStockTakeQty ?? 0)} + + ) : null} + + {!detail.firstStockTakeQty && + !detail.secondStockTakeQty && + !isEditing && ( + + - + )} - ) ={" "} - {formatNumber(detail.firstStockTakeQty ?? 0)} - - ) : null} + + - {/* Second */} + {/* Remark */} + {isEditing && isSecondSubmit ? ( - - {t("Second")}: + <> + {t("Remark")} setSecondQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Stock Take Qty")} + value={remark} + onChange={(e) => setRemark(e.target.value)} + sx={{ width: 150 }} /> - setSecondBadQty(e.target.value)} - sx={{ - width: 130, - minWidth: 130, - "& .MuiInputBase-input": { - height: "1.4375em", - padding: "4px 8px", - }, - }} - placeholder={t("Bad Qty")} - /> - - = - {formatNumber( - parseFloat(secondQty || "0") - - parseFloat(secondBadQty || "0") - )} - - - ) : detail.secondStockTakeQty != null ? ( + + ) : ( - {t("Second")}:{" "} - {formatNumber( - (detail.secondStockTakeQty ?? 0) + - (detail.secondBadQty ?? 0) - )}{" "} - ( - {formatNumber( - detail.secondBadQty ?? 0 - )} - ) ={" "} - {formatNumber(detail.secondStockTakeQty ?? 0)} + {detail.remarks || "-"} - ) : null} - - {!detail.firstStockTakeQty && - !detail.secondStockTakeQty && - !isEditing && ( - - - - - )} - - - - {/* Remark */} - - {isEditing && isSecondSubmit ? ( - <> - {t("Remark")} - + + {detail.uom || "-"} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + setRemark(e.target.value)} - sx={{ width: 150 }} + label={t(detail.stockTakeRecordStatus || "")} + color="default" /> - - ) : ( - - {detail.remarks || "-"} - - )} - - - {detail.uom || "-"} - - - {detail.stockTakeRecordStatus === "pass" ? ( - - ) : detail.stockTakeRecordStatus === "notMatch" ? ( - - ) : ( - - )} - - - - {isEditing ? ( - + )} + + + + {isEditing ? ( + + + + + ) : ( - - - ) : ( - - )} - - - ); - }) - )} - -
-
+ )} + + + ); + }) + )} + + + + + )} );