Sfoglia il codice sorgente

update stock record

master
CANCERYS\kw093 3 settimane fa
parent
commit
e7b5a60858
14 ha cambiato i file con 2338 aggiunte e 816 eliminazioni
  1. +25
    -0
      src/app/(main)/stockRecord/page.tsx
  2. +57
    -1
      src/app/api/jo/actions.ts
  3. +87
    -19
      src/app/api/stockTake/actions.ts
  4. +3
    -0
      src/components/Jodetail/JodetailSearch.tsx
  5. +381
    -0
      src/components/Jodetail/MaterialPickStatusTable.tsx
  6. +329
    -0
      src/components/ProductionProcess/JobProcessStatus.tsx
  7. +5
    -0
      src/components/ProductionProcess/ProductionProcessPage.tsx
  8. +444
    -0
      src/components/StockRecord/SearchPage.tsx
  9. +26
    -0
      src/components/StockRecord/index.tsx
  10. +1
    -17
      src/components/StockTakeManagement/ApproverCardList.tsx
  11. +381
    -253
      src/components/StockTakeManagement/ApproverStockTake.tsx
  12. +1
    -17
      src/components/StockTakeManagement/PickerCardList.tsx
  13. +255
    -227
      src/components/StockTakeManagement/PickerReStockTake.tsx
  14. +343
    -282
      src/components/StockTakeManagement/PickerStockTake.tsx

+ 25
- 0
src/app/(main)/stockRecord/page.tsx Vedi File

@@ -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 (
<>
<I18nProvider namespaces={["inventory", "common"]}>
<Suspense fallback={<SearchPage.Loading />}>
<SearchPage />
</Suspense>
</I18nProvider>
</>
);
};

export default SearchView;

+ 57
- 1
src/app/api/jo/actions.ts Vedi File

@@ -724,6 +724,7 @@ export const fetchAllJoborderProductProcessInfo = cache(async () => {
}
);
});

/*
export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => {
return serverFetchJson<UpdateProductProcessLineQtyResponse>(
@@ -1167,4 +1168,59 @@ export const updateProductProcessLineProcessingTimeSetupTimeChangeoverTime = asy
headers: { "Content-Type": "application/json" },
}
);
};
};
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<MaterialPickStatusItem[]> => {
return await serverFetchJson<MaterialPickStatusItem[]>(
`${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<JobProcessStatusResponse[]>(
`${BASE_API_URL}/product-process/Demo/JobProcessStatus`,
{
method: "GET",
next: { tags: ["jobProcessStatus"] },
}
);
});

;

+ 87
- 19
src/app/api/stockTake/actions.ts Vedi File

@@ -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<T> {
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<InventoryLotDetailResponse[]>(
const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
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<AllPickedStockTakeListReponse[]>( // 改为 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<InventoryLotDetailResponse[]>(
const response = await serverFetchJson<RecordsRes<InventoryLotDetailResponse>>(
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<StockTransactionResponse>;
}

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<RecordsRes<StockTransactionResponse>>(
url,
{
method: "GET",
next: { tags: ["Stock Transaction List"] },
}
);
// 确保返回正确的格式
return response?.records || [];
});


+ 3
- 0
src/components/Jodetail/JodetailSearch.tsx Vedi File

@@ -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<Props> = ({ pickOrders, printerCombo }) => {
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Jo Pick Order Detail")} iconPosition="end" />
<Tab label={t("Complete Job Order Record")} iconPosition="end" />
<Tab label={t("Material Pick Status")} iconPosition="end" />
</Tabs>
</Box>

@@ -503,6 +505,7 @@ const JodetailSearch: React.FC<Props> = ({ pickOrders, printerCombo }) => {
printQty={printQty}
/>
)}
{tabIndex === 2 && <MaterialPickStatusTable />}
</Box>
</Box>
);


+ 381
- 0
src/components/Jodetail/MaterialPickStatusTable.tsx Vedi File

@@ -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<MaterialPickStatusItem[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const refreshCountRef = useRef<number>(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<HTMLInputElement>) => {
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 (
<Card sx={{ mb: 2 }}>
<CardContent>
{/* Title */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
{t("Material Pick Status")}
</Typography>
</Box>



<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<>
<TableContainer component={Paper}>
<Table size="small" sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Pick Order No.- Job Order No.- Item")}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Job Order Qty")}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("No. of Items to be Picked")}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("No. of Items with Issue During Pick")}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Pick Start Time")}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Pick End Time")}
</Typography>
</Box>
</TableCell>
<TableCell sx={{
}}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Pick Time Taken (minutes)")}
</Typography>
</Box>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
{t("No data available")}
</TableCell>
</TableRow>
) : (
paginatedData.map((row) => {
const pickTimeTaken = calculatePickTime(row.pickStartTime, row.pickEndTime);
return (
<TableRow key={row.id}>
<TableCell>
<Box> {row.pickOrderCode || '-'}</Box>
<br />
<Box>{row.jobOrderCode || '-'}</Box>
<br />
<Box>{row.itemCode || '-'} {row.itemName || '-'}</Box>
</TableCell>
<TableCell>
{row.jobOrderQty !== null && row.jobOrderQty !== undefined
? `${row.jobOrderQty} ${row.uom || ''}`
: '-'}
</TableCell>
<TableCell>{row.numberOfItemsToPick ?? 0}</TableCell>
<TableCell>{row.numberOfItemsWithIssue ?? 0}</TableCell>
<TableCell>{formatTime(row.pickStartTime) || '-'}</TableCell>
<TableCell>{formatTime(row.pickEndTime) || '-'}</TableCell>
<TableCell sx={{
backgroundColor: 'rgba(76, 175, 80, 0.1)',
fontWeight: 600
}}>
{pickTimeTaken > 0 ? `${pickTimeTaken} ${t("minutes")}` : '-'}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
{data.length > 0 && (
<TablePagination
component="div"
count={data.length}
page={paginationController.pageNum}
rowsPerPage={paginationController.pageSize}
onPageChange={handlePageChange}
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 15, 25]}
labelRowsPerPage={t("Rows per page")}
/>
)}
</>
)}
</Box>
</CardContent>
</Card>
);
};

export default MaterialPickStatusTable;

+ 329
- 0
src/components/ProductionProcess/JobProcessStatus.tsx Vedi File

@@ -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<JobProcessStatusResponse[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const refreshCountRef = useRef<number>(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 (
<Card sx={{ mb: 2 }}>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
{t("Job Process Status", { ns: "jobProcessStatus" })}
</Typography>
</Box>

<Box sx={{ mt: 2 }}>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table size="small" sx={{ minWidth: 1200 }}>
<TableHead>
<TableRow>
<TableCell rowSpan={3}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Job Order No.", { ns: "jobProcessStatus" })}
</Typography>
</TableCell>
<TableCell rowSpan={3}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("FG / WIP Item", { ns: "jobProcessStatus" })}
</Typography>
</TableCell>
<TableCell rowSpan={3}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Production Time Remaining", { ns: "jobProcessStatus" })}
</Typography>
</TableCell>
<TableCell colSpan={6} align="center">
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Process Status / Time [hh:mm]", { ns: "jobProcessStatus" })}
</Typography>
</TableCell>
</TableRow>
<TableRow>
{[1, 2, 3, 4, 5, 6].map((num) => (
<TableCell key={num} align="center">
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{t("Process", { ns: "jobProcessStatus" })} {num}
</Typography>
</TableCell>
))}
</TableRow>
<TableRow>
{[1, 2, 3, 4, 5, 6].map((num) => (
<TableCell key={num} align="center">
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
{t("Start", { ns: "jobProcessStatus" })}
</Typography>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
{t("Finish", { ns: "jobProcessStatus" })}
</Typography>
<Typography variant="caption" sx={{ fontWeight: 600 }}>
{t("Wait Time [minutes]", { ns: "jobProcessStatus" })}
</Typography>
</Box>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
{t("No data available")}
</TableCell>
</TableRow>
) : (
data.map((row) => (
<TableRow key={row.jobOrderId}>
<TableCell>
{row.jobOrderCode || '-'}
</TableCell>
<TableCell>
<Box>{row.itemCode || '-'}</Box>
<Box>{row.itemName || '-'}</Box>
</TableCell>
<TableCell>

{calculateRemainingTime(row.planEndTime)}
</TableCell>
{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 (
<TableCell key={index} align="center">
<Typography variant="body2">
N/A
</Typography>
</TableCell>
);
}
// 如果工序是必需的,显示三行(Start、Finish、Wait Time)
return (
<TableCell key={index} align="center">
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography variant="body2">{process.equipmentCode || '-'}</Typography>
<Typography variant="body2">
{formatTime(process.startTime)}
</Typography>
<Typography variant="body2">
{formatTime(process.endTime)}
</Typography>
<Typography variant="body2" sx={{
color: waitTime !== '-' && parseInt(waitTime) > 0 ? 'warning.main' : 'text.primary'
}}>
{waitTime}
</Typography>
</Box>
</TableCell>
);
})}
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
)}
</Box>

</CardContent>
</Card>
);
};

export default JobProcessStatus;

+ 5
- 0
src/components/ProductionProcess/ProductionProcessPage.tsx Vedi File

@@ -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<ProductionProcessPageProps> = ({ printerCo
<Tabs value={tabIndex} onChange={handleTabChange} sx={{ mb: 2 }}>
<Tab label={t("Production Process")} />
<Tab label={t("Finished QC Job Orders")} />
<Tab label={t("Job Process Status")} />
</Tabs>

{tabIndex === 0 && (
@@ -190,6 +192,9 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo
selectedPrinter={selectedPrinter}
/>
)}
{tabIndex === 2 && (
<JobProcessStatus />
)}
</Box>
);
};


+ 444
- 0
src/components/StockRecord/SearchPage.tsx Vedi File

@@ -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<Props> = ({ dataList: initialDataList }) => {
const { t } = useTranslation("inventory");

// 添加数据状态
const [dataList, setDataList] = useState<StockTransactionResponse[]>(initialDataList);
const [loading, setLoading] = useState(false);
const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
const isInitialMount = useRef(true);

// 添加分页状态
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>(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<number, number>(); // 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<ExtendedStockTransaction[]>(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<string, number>,
filterArgs: Record<string, any>,
) => {
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<HTMLInputElement | HTMLTextAreaElement>) => {
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<string>[] = 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<Column<ExtendedStockTransaction>[]>(
() => [
{
name: "formattedDate" as keyof ExtendedStockTransaction,
label: t("Date"),
align: "left",
},
{
name: "itemCode" as keyof ExtendedStockTransaction,
label: t("Item-lotNo"),
align: "left",
renderCell: (item) => (
<Box sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Box>{item.itemCode || "-"} {item.itemName || "-"}</Box>
<Box>{item.lotNo || "-"}</Box>
</Stack>
</Box>
),
},
{
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<string, string>) => {
// 检查是否有搜索条件
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 (
<>
<SearchBox
criteria={searchCriteria}
onSearch={handleSearch}
onReset={handleReset}
/>
{loading && <Box sx={{ p: 2 }}>{t("Loading...")}</Box>}
<SearchResults<ExtendedStockTransaction>
items={paginatedItems}
columns={columns}
pagingController={{ ...pagingController, pageSize: actualPageSizeForTable }}
setPagingController={setPagingController}
totalCount={totalCount}
isAutoPaging={false}
/>
</>
);
};

export default SearchPage;

+ 26
- 0
src/components/StockRecord/index.tsx Vedi File

@@ -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 <SearchPage dataList={dataList || []} />;
};

Wrapper.Loading = GeneralLoading;

export default Wrapper;

+ 1
- 17
src/components/StockTakeManagement/ApproverCardList.tsx Vedi File

@@ -201,23 +201,7 @@ const ApproverCardList: React.FC<ApproverCardListProps> = ({ onCardClick }) => {
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
</Typography>
{session.totalInventoryLotNumber > 0 && (
<Box sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}>
<Typography variant="body2" fontWeight={600}>
{t("Progress")}
</Typography>
<Typography variant="body2" fontWeight={600}>
{completionRate}%
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={completionRate}
sx={{ height: 8, borderRadius: 1 }}
/>
</Box>
)}
</CardContent>

<CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>


+ 381
- 253
src/components/StockTakeManagement/ApproverStockTake.tsx Vedi File

@@ -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<ApproverStockTakeProps> = ({

const [inventoryLotDetails, setInventoryLotDetails] = useState<InventoryLotDetailResponse[]>([]);
const [loadingDetails, setLoadingDetails] = useState(false);
const [showOnlyWithDifference, setShowOnlyWithDifference] = useState(false);

// 每个记录的选择状态,key 为 detail.id
const [qtySelection, setQtySelection] = useState<Record<number, QtySelectionType>>({});
const [approverQty, setApproverQty] = useState<Record<number, string>>({});
@@ -60,28 +65,111 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [updatingStatus, setUpdatingStatus] = useState(false);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>("all");
const [total, setTotal] = useState(0);
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const handleBatchSubmitAllRef = useRef<() => Promise<void>>();

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<HTMLInputElement | HTMLTextAreaElement>) => {
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<number, QtySelectionType> = {};
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<ApproverStockTakeProps> = ({
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<ApproverStockTakeProps> = ({
} 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<ApproverStockTakeProps> = ({
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<ApproverStockTakeProps> = ({
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<ApproverStockTakeProps> = ({
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<ApproverStockTakeProps> = ({
} 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<ApproverStockTakeProps> = ({
maximumFractionDigits: 2
});
};
const uniqueWarehouses = Array.from(
new Set(
inventoryLotDetails
@@ -266,6 +355,7 @@ const ApproverStockTake: React.FC<ApproverStockTakeProps> = ({
.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<ApproverStockTakeProps> = ({
{t("Back to List")}
</Button>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>
<Typography variant="h6" sx={{ mb: 2 }}>
{t("Stock Take Section")}: {selectedSession.stockTakeSession}
{uniqueWarehouses && (
<> {t("Warehouse")}: {uniqueWarehouses}</>
)}
</Typography>

<Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
{t("Batch Save All")}
</Button>
</Stack>
<Stack direction="row" spacing={2} alignItems="center">
<Button
variant={showOnlyWithDifference ? "contained" : "outlined"}
color="primary"
onClick={() => setShowOnlyWithDifference(!showOnlyWithDifference)}
startIcon={
<Checkbox
checked={showOnlyWithDifference}
onChange={(e) => setShowOnlyWithDifference(e.target.checked)}
sx={{ p: 0, pointerEvents: 'none' }}
/>
}
sx={{ textTransform: 'none' }}
>
{t("Only Variance")}
</Button>
<Button variant="contained" color="primary" onClick={handleBatchSubmitAll} disabled={batchSaving}>
{t("Batch Save All")}
</Button>
</Stack>
</Stack>
{loadingDetails ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{inventoryLotDetails.length === 0 ? (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
) : (
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";
</TableHead>
<TableBody>
{filteredDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
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 (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
{/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/}
</Stack>
</TableCell>
<TableCell sx={{ minWidth: 300 }}>
{detail.finalQty != null ? (
return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber((detail.finalQty || 0) - (detail.availableQty || 0))}
</Typography>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
</Stack>
) : (
<Stack spacing={1}>
{hasFirst && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "first"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })}
/>
<Typography variant="body2">
{t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)}
</Typography>
</Stack>
)}

{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "second"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })}
/>
<Typography variant="body2">
{t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)}
</Typography>
</Stack>
)}
{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "approver"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "approver" })}
/>
<Typography variant="body2">{t("Approver Input")}:</Typography>
<TextField
size="small"
type="number"
value={approverQty[detail.id] || ""}
onChange={(e) => 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"}
/>
<TextField
size="small"
type="number"
value={approverBadQty[detail.id] || ""}
onChange={(e) => 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"}
/>
<Typography variant="body2">
={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
</Typography>
</Stack>
)}
{(() => {
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 (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)}
</Typography>
);
})()}
</Stack>
)}
</TableCell>
<TableCell>
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
<TableCell>
{detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && (
<Box>
<Button
size="small"
variant="outlined"
color="warning"
onClick={() => handleUpdateStatusToNotMatch(detail)}
disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"}
>
{t("ReStockTake")}
</Button>
</Box>
)}
<br/>
{detail.finalQty == null && (
<Box>
<Button
size="small"
variant="contained"
onClick={() => handleSaveApproverStockTake(detail)}
disabled={saving || submitDisabled || detail.stockTakeRecordStatus === "completed"}
>
{t("Save")}
</Button>
</Box>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
</TableCell>
<TableCell sx={{ minWidth: 300 }}>
{detail.finalQty != null ? (
<Stack spacing={0.5}>
{(() => {
const finalDifference = (detail.finalQty || 0) - (detail.availableQty || 0);
const differenceColor = finalDifference > 0
? 'error.main'
: finalDifference < 0
? 'error.main'
: 'success.main';
return (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
{t("Difference")}: {formatNumber(detail.finalQty)} - {formatNumber(detail.availableQty)} = {formatNumber(finalDifference)}
</Typography>
);
})()}
</Stack>
) : (
<Stack spacing={1}>
{hasFirst && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "first"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "first" })}
/>
<Typography variant="body2">
{t("First")}: {formatNumber((detail.firstStockTakeQty??0)+(detail.firstBadQty??0))} ({detail.firstBadQty??0}) = {formatNumber(detail.firstStockTakeQty??0)}
</Typography>
</Stack>
)}
{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "second"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "second" })}
/>
<Typography variant="body2">
{t("Second")}: {formatNumber((detail.secondStockTakeQty??0)+(detail.secondBadQty??0))} ({detail.secondBadQty??0}) = {formatNumber(detail.secondStockTakeQty??0)}
</Typography>
</Stack>
)}
{hasSecond && (
<Stack direction="row" spacing={1} alignItems="center">
<Radio
size="small"
checked={selection === "approver"}
onChange={() => setQtySelection({ ...qtySelection, [detail.id]: "approver" })}
/>
<Typography variant="body2">{t("Approver Input")}:</Typography>
<TextField
size="small"
type="number"
value={approverQty[detail.id] || ""}
onChange={(e) => 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"}
/>
<TextField
size="small"
type="number"
value={approverBadQty[detail.id] || ""}
onChange={(e) => 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"}
/>
<Typography variant="body2">
={(parseFloat(approverQty[detail.id] || "0") - parseFloat(approverBadQty[detail.id] || "0"))}
</Typography>
</Stack>
)}
{(() => {
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 (
<Typography variant="body2" sx={{ fontWeight: 'bold', color: differenceColor }}>
{t("Difference")}: {t("selected stock take qty")}({formatNumber(selectedQty)}) - {t("book qty")}({formatNumber(bookQty)}) = {formatNumber(difference)}
</Typography>
);
})()}
</Stack>
)}
</TableCell>
<TableCell>
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
<TableCell>
{detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && (
<Box>
<Button
size="small"
variant="outlined"
color="warning"
onClick={() => handleUpdateStatusToNotMatch(detail)}
disabled={updatingStatus || detail.stockTakeRecordStatus === "completed"}
>
{t("ReStockTake")}
</Button>
</Box>
)}
<br/>
{detail.finalQty == null && (
<Box>
<Button
size="small"
variant="contained"
onClick={() => handleSaveApproverStockTake(detail)}
disabled={saving || submitDisabled || detail.stockTakeRecordStatus === "completed"}
>
{t("Save")}
</Button>
</Box>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
</>
)}
</Box>
);


+ 1
- 17
src/components/StockTakeManagement/PickerCardList.tsx Vedi File

@@ -224,23 +224,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockT
{t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography>
{session.totalInventoryLotNumber > 0 && (
<Box sx={{ mt: 2 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}>
<Typography variant="body2" fontWeight={600}>
{t("Progress")}
</Typography>
<Typography variant="body2" fontWeight={600}>
{completionRate}%
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={completionRate}
sx={{ height: 8, borderRadius: 1 }}
/>
</Box>
)}
</CardContent>

<CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>


+ 255
- 227
src/components/StockTakeManagement/PickerReStockTake.tsx Vedi File

@@ -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<PickerStockTakeProps> = ({
const PickerReStockTake: React.FC<PickerReStockTakeProps> = ({
selectedSession,
onBack,
onSnackbar,
@@ -60,28 +61,63 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [shortcutInput, setShortcutInput] = useState<string>("");
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>("all");
const [total, setTotal] = useState(0);

const currentUserId = session?.id ? parseInt(session.id) : undefined;
const handleBatchSubmitAllRef = useRef<() => Promise<void>>();
const handleChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
}, []);
const handleChangeRowsPerPage = useCallback((event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
} 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<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
} 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<PickerStockTakeProps> = ({
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Qty")}</TableCell>
<TableCell>{t("Bad Qty")}</TableCell>
{/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/}
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>

<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{inventoryLotDetails.length === 0 ? (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={12} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Qty")}</TableCell>
<TableCell>{t("Bad Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
) : (
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;
</TableHead>
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
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 (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
{/*<Box><Chip size="small" label={t(detail.status)} color="default" /></Box>*/}
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstStockTakeQty ? (
<Typography variant="body2">
{t("First")}: {detail.firstStockTakeQty.toFixed(2)}
</Typography>
) : null}
return (
<TableRow key={detail.id}>
<TableCell>{detail.warehouseArea || "-"}{detail.warehouseSlot || "-"}</TableCell>
<TableCell sx={{
maxWidth: 150,
wordBreak: 'break-word',
whiteSpace: 'normal',
lineHeight: 1.5
}}>
<Stack spacing={0.5}>
<Box>{detail.itemCode || "-"} {detail.itemName || "-"}</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>{detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"}</Box>
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstStockTakeQty ? (
<Typography variant="body2">
{t("First")}: {detail.firstStockTakeQty.toFixed(2)}
</Typography>
) : null}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondStockTakeQty ? (
<Typography variant="body2">
{t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty != null && detail.firstBadQty > 0 ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
</Typography>
) : (
<Typography variant="body2" sx={{ visibility: 'hidden' }}>
{t("First")}: 0.00
</Typography>
)}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty != null && detail.secondBadQty > 0 ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondStockTakeQty ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
/>
</>
) : (
<Typography variant="body2">
{t("Second")}: {detail.secondStockTakeQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
{detail.remarks || "-"}
</Typography>
)}
</Stack>
</TableCell>
<TableCell>
<Stack spacing={0.5}>
{isEditing && isFirstSubmit ? (
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.firstBadQty != null && detail.firstBadQty > 0 ? (
<Typography variant="body2">
{t("First")}: {detail.firstBadQty.toFixed(2)}
</Typography>
) : (
<Typography variant="body2" sx={{ visibility: 'hidden' }}>
{t("First")}: 0.00
</Typography>
)}
{isEditing && isSecondSubmit ? (
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{ width: 100 }}
/>
) : detail.secondBadQty != null && detail.secondBadQty > 0 ? (
<Typography variant="body2">
{t("Second")}: {detail.secondBadQty.toFixed(2)}
</Typography>
) : null}
{!detail.firstBadQty && !detail.secondBadQty && !isEditing && (
<Typography variant="body2" color="text.secondary">
-
</Typography>
)}
</Stack>
</TableCell>
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
// If you want a single-line input, remove multiline/rows:
// multiline
// rows={2}
/>
</>
) : (
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>
</TableCell>
<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled}
>
{t("Save")}
</Button>
<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="success" />
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip size="small" label={t(detail.stockTakeRecordStatus)} color="warning" />
) : (
<Chip size="small" label={t(detail.stockTakeRecordStatus || "")} color="default" />
)}
</TableCell>
<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled}
>
{t("Save")}
</Button>
<Button
size="small"
onClick={handleCancelEdit}
>
{t("Cancel")}
</Button>
</Stack>
) : (
<Button
size="small"
onClick={handleCancelEdit}
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{t("Cancel")}
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
</Stack>
) : (
<Button
size="small"
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
</>
)}
</Box>
);
};

export default PickerStockTake;
export default PickerReStockTake;

+ 343
- 282
src/components/StockTakeManagement/PickerStockTake.tsx Vedi File

@@ -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<PickerStockTakeProps> = ({
const [saving, setSaving] = useState(false);
const [batchSaving, setBatchSaving] = useState(false);
const [shortcutInput, setShortcutInput] = useState<string>("");
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState<number | string>("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<void>>();

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<number>) => {
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<HTMLInputElement | HTMLTextAreaElement>) => {
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<PickerStockTakeProps> = ({

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<PickerStockTakeProps> = ({
t,
currentUserId,
onSnackbar,
loadDetails,
page,
pageSize,
]
);

@@ -243,11 +296,7 @@ const PickerStockTake: React.FC<PickerStockTakeProps> = ({
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<PickerStockTakeProps> = ({
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{inventoryLotDetails.length === 0 ? (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
<TableCell>{t("Warehouse Location")}</TableCell>
<TableCell>{t("Item-lotNo-ExpiryDate")}</TableCell>
<TableCell>{t("Stock Take Qty(include Bad Qty)= Available Qty")}</TableCell>
<TableCell>{t("Remark")}</TableCell>
<TableCell>{t("UOM")}</TableCell>
<TableCell>{t("Record Status")}</TableCell>
<TableCell>{t("Action")}</TableCell>
</TableRow>
) : (
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 (
<TableRow key={detail.id}>
<TableCell>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</TableCell>
<TableCell
sx={{
maxWidth: 150,
wordBreak: "break-word",
whiteSpace: "normal",
lineHeight: 1.5,
}}
>
<Stack spacing={0.5}>
<Box>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Stack>
</TableCell>

{/* Qty + Bad Qty 合并显示/输入 */}
<TableCell sx={{ minWidth: 300 }}>
<Stack spacing={1}>
{/* First */}
{isEditing && isFirstSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
</TableHead>
<TableBody>
{inventoryLotDetails.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data")}
</Typography>
</TableCell>
</TableRow>
) : (
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 (
<TableRow key={detail.id}>
<TableCell>
{detail.warehouseArea || "-"}
{detail.warehouseSlot || "-"}
</TableCell>
<TableCell
sx={{
maxWidth: 150,
wordBreak: "break-word",
whiteSpace: "normal",
lineHeight: 1.5,
}}
>
<Stack spacing={0.5}>
<Box>
{detail.itemCode || "-"} {detail.itemName || "-"}
</Box>
<Box>{detail.lotNo || "-"}</Box>
<Box>
{detail.expiryDate
? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT)
: "-"}
</Box>
</Stack>
</TableCell>

{/* Qty + Bad Qty 合并显示/输入 */}
<TableCell sx={{ minWidth: 300 }}>
<Stack spacing={1}>
{/* First */}
{isEditing && isFirstSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("First")}:</Typography>
<TextField
size="small"
type="number"
value={firstQty}
onChange={(e) => setFirstQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={firstBadQty}
onChange={(e) => setFirstBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(firstQty || "0") -
parseFloat(firstBadQty || "0")
)}
</Typography>
</Stack>
) : detail.firstStockTakeQty != null ? (
<Typography variant="body2">
=
{t("First")}:{" "}
{formatNumber(
(detail.firstStockTakeQty ?? 0) +
(detail.firstBadQty ?? 0)
)}{" "}
(
{formatNumber(
parseFloat(firstQty || "0") -
parseFloat(firstBadQty || "0")
detail.firstBadQty ?? 0
)}
) ={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography>
</Stack>
) : detail.firstStockTakeQty != null ? (
<Typography variant="body2">
{t("First")}:{" "}
{formatNumber(
(detail.firstStockTakeQty ?? 0) +
(detail.firstBadQty ?? 0)
)}{" "}
(
{formatNumber(
detail.firstBadQty ?? 0
) : null}

{/* Second */}
{isEditing && isSecondSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => setSecondQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Stock Take Qty")}
/>
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(secondQty || "0") -
parseFloat(secondBadQty || "0")
)}
</Typography>
</Stack>
) : detail.secondStockTakeQty != null ? (
<Typography variant="body2">
{t("Second")}:{" "}
{formatNumber(
(detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0)
)}{" "}
(
{formatNumber(
detail.secondBadQty ?? 0
)}
) ={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)}
</Typography>
) : null}

{!detail.firstStockTakeQty &&
!detail.secondStockTakeQty &&
!isEditing && (
<Typography
variant="body2"
color="text.secondary"
>
-
</Typography>
)}
) ={" "}
{formatNumber(detail.firstStockTakeQty ?? 0)}
</Typography>
) : null}
</Stack>
</TableCell>

{/* Second */}
{/* Remark */}
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2">{t("Second")}:</Typography>
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
size="small"
type="number"
value={secondQty}
onChange={(e) => 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 }}
/>
<TextField
size="small"
type="number"
value={secondBadQty}
onChange={(e) => setSecondBadQty(e.target.value)}
sx={{
width: 130,
minWidth: 130,
"& .MuiInputBase-input": {
height: "1.4375em",
padding: "4px 8px",
},
}}
placeholder={t("Bad Qty")}
/>
<Typography variant="body2">
=
{formatNumber(
parseFloat(secondQty || "0") -
parseFloat(secondBadQty || "0")
)}
</Typography>
</Stack>
) : detail.secondStockTakeQty != null ? (
</>
) : (
<Typography variant="body2">
{t("Second")}:{" "}
{formatNumber(
(detail.secondStockTakeQty ?? 0) +
(detail.secondBadQty ?? 0)
)}{" "}
(
{formatNumber(
detail.secondBadQty ?? 0
)}
) ={" "}
{formatNumber(detail.secondStockTakeQty ?? 0)}
{detail.remarks || "-"}
</Typography>
) : null}

{!detail.firstStockTakeQty &&
!detail.secondStockTakeQty &&
!isEditing && (
<Typography
variant="body2"
color="text.secondary"
>
-
</Typography>
)}
</Stack>
</TableCell>

{/* Remark */}
<TableCell sx={{ width: 180 }}>
{isEditing && isSecondSubmit ? (
<>
<Typography variant="body2">{t("Remark")}</Typography>
<TextField
)}
</TableCell>

<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip
size="small"
value={remark}
onChange={(e) => setRemark(e.target.value)}
sx={{ width: 150 }}
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
</>
) : (
<Typography variant="body2">
{detail.remarks || "-"}
</Typography>
)}
</TableCell>

<TableCell>{detail.uom || "-"}</TableCell>

<TableCell>
{detail.stockTakeRecordStatus === "pass" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="success"
/>
) : detail.stockTakeRecordStatus === "notMatch" ? (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus)}
color="warning"
/>
) : (
<Chip
size="small"
label={t(detail.stockTakeRecordStatus || "")}
color="default"
/>
)}
</TableCell>

<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
)}
</TableCell>

<TableCell>
{isEditing ? (
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled}
>
{t("Save")}
</Button>
<Button size="small" onClick={handleCancelEdit}>
{t("Cancel")}
</Button>
</Stack>
) : (
<Button
size="small"
variant="contained"
onClick={() => handleSaveStockTake(detail)}
disabled={saving || submitDisabled}
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{t("Save")}
</Button>
<Button size="small" onClick={handleCancelEdit}>
{t("Cancel")}
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
</Stack>
) : (
<Button
size="small"
variant="outlined"
onClick={() => handleStartEdit(detail)}
disabled={submitDisabled}
>
{!detail.stockTakeRecordId
? t("Input")
: detail.stockTakeRecordStatus === "notMatch"
? t("Input")
: t("View")}
</Button>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={total}
page={page}
onPageChange={handleChangePage}
rowsPerPage={pageSize === "all" ? total : (pageSize as number)}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100, { value: -1, label: t("All") }]}
labelRowsPerPage={t("Rows per page")}
/>
</>
)}
</Box>
);


Caricamento…
Annulla
Salva