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 ? (
+
+
+
+
+ ) : (
-
-
- ) : (
-
- )}
-
-
- );
- })
- )}
-
-
-
+ )}
+
+
+ );
+ })
+ )}
+
+
+
+
+ >
)}
);