CANCERYS\kw093 3 miesięcy temu
rodzic
commit
32a5a541b9
14 zmienionych plików z 2280 dodań i 1279 usunięć
  1. +206
    -11
      src/app/api/pickOrder/actions.ts
  2. +1
    -5
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  3. +720
    -66
      src/components/FinishedGoodSearch/GoodPickExecution.tsx
  4. +372
    -0
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  5. +0
    -737
      src/components/FinishedGoodSearch/LotTable.tsx
  6. +1
    -1
      src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx
  7. +1
    -1
      src/components/FinishedGoodSearch/newcreatitem.tsx
  8. +222
    -184
      src/components/PickOrderSearch/LotTable.tsx
  9. +186
    -271
      src/components/PickOrderSearch/PickExecution.tsx
  10. +372
    -0
      src/components/PickOrderSearch/PickExecutionForm.tsx
  11. +196
    -0
      src/components/PickOrderSearch/PickOrderDetailsTable.tsx
  12. +1
    -1
      src/components/PickOrderSearch/PickQcStockInModalVer3.tsx
  13. +1
    -1
      src/components/PickOrderSearch/newcreatitem.tsx
  14. +1
    -1
      src/i18n/zh/pickOrder.json

+ 206
- 11
src/app/api/pickOrder/actions.ts Wyświetl plik

@@ -94,10 +94,12 @@ export interface GetPickOrderInfoResponse {
export interface GetPickOrderInfo {
id: number;
code: string;
targetDate: string;
consoCode: string | null; // ✅ 添加 consoCode 属性
targetDate: string | number[]; // ✅ Support both formats
type: string;
status: string;
assignTo: number;
groupName: string; // ✅ Add this field
pickOrderLines: GetPickOrderLineInfo[];
}

@@ -157,9 +159,126 @@ export interface LotDetailWithStockOutLine {
stockOutLineStatus?: string;
stockOutLineQty?: number;
}
export interface PickAnotherLotFormData {
pickOrderLineId: number;
lotId: number;
qty: number;
type: string;
handlerId?: number;
category?: string;
releasedBy?: number;
recordDate?: string;
}
export const recordFailLot = async (data: PickAnotherLotFormData) => {
const result = await serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/suggestedPickLot/recordFailLot`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return result;
};
export interface PickExecutionIssueData {
pickOrderId: number;
pickOrderCode: string;
pickOrderCreateDate: string;
pickExecutionDate: string;
pickOrderLineId: number;
itemId: number;
itemCode: string;
itemDescription: string;
lotId: number;
lotNo: string;
storeLocation: string;
requiredQty: number;
actualPickQty: number;
missQty: number;
badItemQty: number;
issueRemark: string;
pickerName: string;
handledBy?: number;
}
export interface AutoAssignReleaseResponse {
id: number | null;
name: string;
code: string;
type?: string;
message: string | null;
errorPosition: string;
entity?: {
consoCode?: string;
pickOrderIds?: number[];
hasActiveOrders: boolean;
};
}

export interface PickOrderCompletionResponse {
id: number | null;
name: string;
code: string;
type?: string;
message: string | null;
errorPosition: string;
entity?: {
hasCompletedOrders: boolean;
completedOrders: Array<{
pickOrderId: number;
pickOrderCode: string;
consoCode: string;
isCompleted: boolean;
stockOutStatus: string;
totalLines: number;
unfinishedLines: number;
}>;
allOrders: Array<{
pickOrderId: number;
pickOrderCode: string;
consoCode: string;
isCompleted: boolean;
stockOutStatus: string;
totalLines: number;
unfinishedLines: number;
}>;
};
}

export const autoAssignAndReleasePickOrder = async (userId: number): Promise<AutoAssignReleaseResponse> => {
const response = await serverFetchJson<AutoAssignReleaseResponse>(
`${BASE_API_URL}/pickOrder/auto-assign-release/${userId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return response;
};

export const checkPickOrderCompletion = async (userId: number): Promise<PickOrderCompletionResponse> => {
const response = await serverFetchJson<PickOrderCompletionResponse>(
`${BASE_API_URL}/pickOrder/check-pick-completion/${userId}`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
return response;
};
export const recordPickExecutionIssue = async (data: PickExecutionIssueData) => {
const result = await serverFetchJson<PostPickOrderResponse>(
`${BASE_API_URL}/pickExecution/recordIssue`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
revalidateTag("pickorder");
return result;
};
export const resuggestPickOrder = async (pickOrderId: number) => {
console.log("Resuggesting pick order:", pickOrderId);
const result = await serverFetchJson<PostPickOrderResponse>(
@@ -286,7 +405,7 @@ export interface PickOrderLotDetailResponse {
actualPickQty: number;
suggestedPickLotId: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
}
interface ALLPickOrderLotDetailResponse {
// Pick Order Information
@@ -315,24 +434,62 @@ interface ALLPickOrderLotDetailResponse {
lotNo: string;
expiryDate: string;
location: string;
outQty: number;
holdQty: number;
stockUnit: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
totalPickedByAllPickOrders: number;
suggestedPickLotId: number;
lotStatus: string;
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
processingStatus: string;
}
export const fetchALLPickOrderLineLotDetails = cache(async (userId?: number) => {
interface SuggestionWithStatus {
suggestionId: number;
suggestionQty: number;
suggestionCreated: string;
lotLineId: number;
lotNo: string;
expiryDate: string;
location: string;
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
suggestionStatus: 'active' | 'completed' | 'rejected' | 'in_progress' | 'unknown';
}
export interface CheckCompleteResponse {
id: number | null;
name: string;
code: string;
type?: string;
message: string | null;
errorPosition: string;
}

export const checkAndCompletePickOrderByConsoCode = async (consoCode: string): Promise<CheckCompleteResponse> => {
const response = await serverFetchJson<CheckCompleteResponse>(
`${BASE_API_URL}/pickOrder/check-complete/${consoCode}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
);
revalidateTag("pickorder");
return response;
};
export const fetchPickOrderDetailsOptimized = cache(async (userId?: number) => {
const url = userId
? `${BASE_API_URL}/pickOrder/all-lots-with-details?userId=${userId}`
: `${BASE_API_URL}/pickOrder/all-lots-with-details`;
? `${BASE_API_URL}/pickOrder/detail-optimized?userId=${userId}`
: `${BASE_API_URL}/pickOrder/detail-optimized`;
return serverFetchJson<ALLPickOrderLotDetailResponse[]>(
return serverFetchJson<any[]>(
url,
{
method: "GET",
@@ -340,11 +497,49 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId?: number) =>
},
);
});
export const fetchAllPickOrderDetails = cache(async (userId?: number) => {
const url = userId
? `${BASE_API_URL}/pickOrder/detail?userId=${userId}`
: `${BASE_API_URL}/pickOrder/detail`;
const fetchSuggestionsWithStatus = async (pickOrderLineId: number) => {
try {
const response = await fetch(`/api/suggestedPickLot/suggestions-with-status/${pickOrderLineId}`);
const suggestions: SuggestionWithStatus[] = await response.json();
return suggestions;
} catch (error) {
console.error('Error fetching suggestions with status:', error);
return [];
}
};

export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Promise<any[]> => {
try {
console.log("🔍 Fetching all pick order line lot details for userId:", userId);
// ✅ 使用 serverFetchJson 而不是直接的 fetch
const data = await serverFetchJson<any[]>(
`${BASE_API_URL}/pickOrder/all-lots-with-details/${userId}`,
{
method: 'GET',
next: { tags: ["pickorder"] },
}
);
console.log("✅ API Response:", data);
return data;
} catch (error) {
console.error("❌ Error fetching all pick order line lot details:", error);
throw error;
}
});
export const fetchAllPickOrderDetails = cache(async (userId?: number) => {
if (!userId) {
return {
consoCode: null,
pickOrders: [],
items: []
};
}
// ✅ Use the correct endpoint with userId in the path
const url = `${BASE_API_URL}/pickOrder/detail-optimized/${userId}`;
return serverFetchJson<GetPickOrderInfoResponse>(
url,
{


+ 1
- 5
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Wyświetl plik

@@ -271,8 +271,6 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
borderBottom: '1px solid #e0e0e0'
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Assign")} iconPosition="end" />
<Tab label={t("Release")} iconPosition="end" />
<Tab label={t("Pick Execution")} iconPosition="end" />
</Tabs>
</Box>
@@ -281,9 +279,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
<Box sx={{
p: 2
}}>
{tabIndex === 2 && <PickExecution filterArgs={filterArgs} />}
{tabIndex === 0 && <AssignAndRelease filterArgs={filterArgs} />}
{tabIndex === 1 && <AssignTo filterArgs={filterArgs} />}
{tabIndex === 0 && <PickExecution filterArgs={filterArgs} />}
</Box>
</Box>
);


+ 720
- 66
src/components/FinishedGoodSearch/GoodPickExecution.tsx
Plik diff jest za duży
Wyświetl plik


+ 372
- 0
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx Wyświetl plik

@@ -0,0 +1,372 @@
// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
"use client";

import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
import { fetchEscalationCombo } from "@/app/api/user/actions";

interface LotPickData {
id: number;
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
inQty: number;
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
}

interface PickExecutionFormProps {
open: boolean;
onClose: () => void;
onSubmit: (data: PickExecutionIssueData) => Promise<void>;
selectedLot: LotPickData | null;
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// ✅ Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}

// 定义错误类型
interface FormErrors {
actualPickQty?: string;
missQty?: string;
badItemQty?: string;
issueRemark?: string;
handledBy?: string;
}

const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
open,
onClose,
onSubmit,
selectedLot,
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// ✅ Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);

// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
return Math.max(0, remainingQty);
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
const requiredQty = lot.requiredQty-(lot.actualPickQty||0);
return Math.max(0, requiredQty);
}, []);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
try {
const escalationCombo = await fetchEscalationCombo();
setHandlers(escalationCombo);
} catch (error) {
console.error("Error fetching handlers:", error);
}
};
fetchHandlers();
}, []);

// 初始化表单数据 - 每次打开时都重新初始化
useEffect(() => {
if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
const getSafeDate = (dateValue: any): string => {
if (!dateValue) return new Date().toISOString().split('T')[0];
try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
return new Date().toISOString().split('T')[0];
}
return date.toISOString().split('T')[0];
} catch {
return new Date().toISOString().split('T')[0];
}
};

// 计算剩余可用数量
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
console.log("=== PickExecutionForm Debug ===");
console.log("selectedLot:", selectedLot);
console.log("inQty:", selectedLot.inQty);
console.log("outQty:", selectedLot.outQty);
console.log("holdQty:", selectedLot.holdQty);
console.log("availableQty:", selectedLot.availableQty);
console.log("calculated remainingAvailableQty:", remainingAvailableQty);
console.log("=== End Debug ===");
setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
pickExecutionDate: new Date().toISOString().split('T')[0],
pickOrderLineId: selectedPickOrderLine.id,
itemId: selectedPickOrderLine.itemId,
itemCode: selectedPickOrderLine.itemCode,
itemDescription: selectedPickOrderLine.itemName,
lotId: selectedLot.lotId,
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: selectedLot.actualPickQty || 0,
missQty: 0,
badItemQty: 0, // 初始化为 0,用户需要手动输入
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除错误
if (errors[field as keyof FormErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
}, [errors]);

// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired');
}
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems');
newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems');
}
if (formData.missQty && formData.missQty < 0) {
newErrors.missQty = t('pickOrder.validation.missQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty < 0) {
newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) {
newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) {
newErrors.handledBy = t('pickOrder.validation.handlerRequired');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (!validateForm() || !formData.pickOrderId) {
return;
}

setLoading(true);
try {
await onSubmit(formData as PickExecutionIssueData);
onClose();
} catch (error) {
console.error('Error submitting pick execution issue:', error);
} finally {
setLoading(false);
}
};

const handleClose = () => {
setFormData({});
setErrors({});
onClose();
};

if (!selectedLot || !selectedPickOrderLine) {
return null;
}

const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* ✅ Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
<Typography variant="body2" color="warning.main">
<strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
</Typography>
</Box>
</Grid>
{/* ✅ Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
label={t('requiredQty')}
value={requiredQty || 0}
disabled
variant="outlined"
helperText={t('Still need to pick')}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('remainingAvailableQty')}
value={remainingAvailableQty}
disabled
variant="outlined"
helperText={t('Available in warehouse')}
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('actualPickQty')}
type="number"
value={formData.actualPickQty || 0}
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || t('Enter the quantity actually picked')}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('missQty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
error={!!errors.missQty}
helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('badItemQty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
error={!!errors.badItemQty}
helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
variant="outlined"
/>
</Grid>
{/* ✅ Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) && (
<>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
label={t('issueRemark')}
multiline
rows={4}
value={formData.issueRemark || ''}
onChange={(e) => handleInputChange('issueRemark', e.target.value)}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
placeholder={t('Describe the issue with bad items')}
variant="outlined"
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth error={!!errors.handledBy}>
<InputLabel>{t('handler')}</InputLabel>
<Select
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
label={t('handler')}
>
{handlers.map((handler) => (
<MenuItem key={handler.id} value={handler.id.toString()}>
{handler.name}
</MenuItem>
))}
</Select>
{errors.handledBy && (
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
{errors.handledBy}
</Typography>
)}
</FormControl>
</Grid>
</>
)}
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={loading}
>
{loading ? t('submitting') : t('submit')}
</Button>
</DialogActions>
</Dialog>
);
};

export default PickExecutionForm;

+ 0
- 737
src/components/FinishedGoodSearch/LotTable.tsx Wyświetl plik

@@ -1,737 +0,0 @@
"use client";

import {
Box,
Button,
Checkbox,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
TablePagination,
Modal,
} from "@mui/material";
import { useCallback, useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions";
import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
interface LotPickData {
id: number;
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
}

interface PickQtyData {
[lineId: number]: {
[lotId: number]: number;
};
}

interface LotTableProps {
lotData: LotPickData[];
selectedRowId: number | null;
selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickQtyData: PickQtyData;
selectedLotRowId: string | null;
selectedLotId: number | null;
onLotSelection: (uniqueLotId: string, lotId: number) => void;
onPickQtyChange: (lineId: number, lotId: number, value: number) => void;
onSubmitPickQty: (lineId: number, lotId: number) => void;
onCreateStockOutLine: (inventoryLotLineId: number) => void;
onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void;
onLotSelectForInput: (lot: LotPickData) => void;
showInputBody: boolean;
setShowInputBody: (show: boolean) => void;
selectedLotForInput: LotPickData | null;
generateInputBody: () => any;
onDataRefresh: () => Promise<void>;
}

// ✅ QR Code Modal Component
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
lot: LotPickData | null;
onQrCodeSubmit: (lotNo: string) => void;
}> = ({ open, onClose, lot, onQrCodeSubmit }) => {
const { t } = useTranslation("pickOrder");
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [manualInput, setManualInput] = useState<string>('');
// ✅ Add state to track manual input submission
const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
const [manualInputError, setManualInputError] = useState<boolean>(false);

// ✅ Process scanned QR codes
useEffect(() => {
if (qrValues.length > 0 && lot) {
const latestQr = qrValues[qrValues.length - 1];
const qrContent = latestQr.replace(/[{}]/g, '');
if (qrContent === lot.lotNo) {
onQrCodeSubmit(lot.lotNo);
onClose();
resetScan();
} else {
// ✅ Set error state for helper text
setManualInputError(true);
setManualInputSubmitted(true);
}
}
}, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]);

// ✅ Clear states when modal opens or lot changes
useEffect(() => {
if (open) {
setManualInput('');
setManualInputSubmitted(false);
setManualInputError(false);
}
}, [open]);

useEffect(() => {
if (lot) {
setManualInput('');
setManualInputSubmitted(false);
setManualInputError(false);
}
}, [lot]);

{/*
const handleManualSubmit = () => {
if (manualInput.trim() === lot?.lotNo) {
// ✅ Success - no error helper text needed
onQrCodeSubmit(lot.lotNo);
onClose();
setManualInput('');
} else {
// ✅ Show error helper text after submit
setManualInputError(true);
setManualInputSubmitted(true);
// Don't clear input - let user see what they typed
}
};

return (
<Modal open={open} onClose={onClose}>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
p: 3,
borderRadius: 2,
minWidth: 400,
}}>
<Typography variant="h6" gutterBottom>
{t("QR Code Scan for Lot")}: {lot?.lotNo}
</Typography>
<Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
<Typography variant="body2" gutterBottom>
<strong>Scanner Status:</strong> {isScanning ? 'Scanning...' : 'Ready'}
</Typography>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={isScanning ? stopScan : startScan}
size="small"
>
{isScanning ? 'Stop Scan' : 'Start Scan'}
</Button>
<Button
variant="outlined"
onClick={resetScan}
size="small"
>
Reset
</Button>
</Stack>
</Box>

<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>Manual Input:</strong>
</Typography>
<TextField
fullWidth
size="small"
value={manualInput}
onChange={(e) => setManualInput(e.target.value)}
sx={{ mb: 1 }}
// ✅ Only show error after submit button is clicked
error={manualInputSubmitted && manualInputError}
helperText={
// ✅ Show helper text only after submit with error
manualInputSubmitted && manualInputError
? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}`
: ''
}
/>
<Button
variant="contained"
onClick={handleManualSubmit}
disabled={!manualInput.trim()}
size="small"
color="primary"
>
Submit Manual Input
</Button>
</Box>

{qrValues.length > 0 && (
<Box sx={{ mb: 2, p: 2, backgroundColor: manualInputError ? '#ffebee' : '#e8f5e8', borderRadius: 1 }}>
<Typography variant="body2" color={manualInputError ? 'error' : 'success'}>
<strong>QR Scan Result:</strong> {qrValues[qrValues.length - 1]}
</Typography>
{manualInputError && (
<Typography variant="caption" color="error" display="block">
❌ Mismatch! Expected: {lot?.lotNo}
</Typography>
)}
</Box>
)}

<Box sx={{ mt: 2, textAlign: 'right' }}>
<Button onClick={onClose} variant="outlined">
Cancel
</Button>
</Box>
</Box>
</Modal>
);
};
*/}
useEffect(() => {
if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '') {
// Auto-submit when manual input matches the expected lot number
console.log('🔄 Auto-submitting manual input:', manualInput.trim());
// Add a small delay to ensure proper execution order
const timer = setTimeout(() => {
onQrCodeSubmit(lot.lotNo);
onClose();
setManualInput('');
setManualInputError(false);
setManualInputSubmitted(false);
}, 200); // 200ms delay
return () => clearTimeout(timer);
}
}, [manualInput, lot, onQrCodeSubmit, onClose]);
const handleManualSubmit = () => {
if (manualInput.trim() === lot?.lotNo) {
// ✅ Success - no error helper text needed
onQrCodeSubmit(lot.lotNo);
onClose();
setManualInput('');
} else {
// ✅ Show error helper text after submit
setManualInputError(true);
setManualInputSubmitted(true);
// Don't clear input - let user see what they typed
}
};
useEffect(() => {
if (open) {
startScan();
}
}, [open, startScan]);
return (
<Modal open={open} onClose={onClose}>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
p: 3,
borderRadius: 2,
minWidth: 400,
}}>
<Typography variant="h6" gutterBottom>
QR Code Scan for Lot: {lot?.lotNo}
</Typography>
{/* Manual Input with Submit-Triggered Helper Text */}
<Box sx={{ mb: 2 }}>
<Typography variant="body2" gutterBottom>
<strong>Manual Input:</strong>
</Typography>
<TextField
fullWidth
size="small"
value={manualInput}
onChange={(e) => setManualInput(e.target.value)}
sx={{ mb: 1 }}
error={manualInputSubmitted && manualInputError}
helperText={
manualInputSubmitted && manualInputError
? `The input is not the same as the expected lot number.`
: ''
}
/>
<Button
variant="contained"
onClick={handleManualSubmit}
disabled={!manualInput.trim()}
size="small"
color="primary"
>
Submit Manual Input
</Button>
</Box>

{/* Show QR Scan Status */}
{qrValues.length > 0 && (
<Box sx={{ mb: 2, p: 2, backgroundColor: manualInputError ? '#ffebee' : '#e8f5e8', borderRadius: 1 }}>
<Typography variant="body2" color={manualInputError ? 'error' : 'success'}>
<strong>QR Scan Result:</strong> {qrValues[qrValues.length - 1]}
</Typography>
{manualInputError && (
<Typography variant="caption" color="error" display="block">
❌ Mismatch! Expected!
</Typography>
)}
</Box>
)}

<Box sx={{ mt: 2, textAlign: 'right' }}>
<Button onClick={onClose} variant="outlined">
Cancel
</Button>
</Box>
</Box>
</Modal>
);
};


const LotTable: React.FC<LotTableProps> = ({
lotData,
selectedRowId,
selectedRow,
pickQtyData,
selectedLotRowId,
selectedLotId,
onLotSelection,
onPickQtyChange,
onSubmitPickQty,
onCreateStockOutLine,
onQcCheck,
onLotSelectForInput,
showInputBody,
setShowInputBody,
selectedLotForInput,
generateInputBody,
onDataRefresh,
}) => {
const { t } = useTranslation("pickOrder");
// ✅ Add QR scanner context
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
// ✅ Add state for QR input modal
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null);
const [manualQrInput, setManualQrInput] = useState<string>('');
// 分页控制器
const [lotTablePagingController, setLotTablePagingController] = useState({
pageNum: 0,
pageSize: 10,
});

// ✅ 添加状态消息生成函数
const getStatusMessage = useCallback((lot: LotPickData) => {
if (!lot.stockOutLineId) {
return t("Please finish QR code scan, QC check and pick order.");
}
switch (lot.stockOutLineStatus?.toLowerCase()) {
case 'pending':
return t("Please submit pick order.");
case 'checked':
return t("Please submit the pick order.");
case 'partially_completed':
return t("Partial quantity submitted. Please submit more or complete the order.") ;
case 'completed':
return t("Pick order completed successfully!");
case 'rejected':
return t("QC check failed. Lot has been rejected and marked as unavailable.");
case 'unavailable':
return t("This order is insufficient, please pick another lot.");
default:
return t("Please finish QR code scan, QC check and pick order.");
}
}, []);

const prepareLotTableData = useMemo(() => {
return lotData.map((lot) => ({
...lot,
id: lot.lotId,
}));
}, [lotData]);

// 分页数据
const paginatedLotTableData = useMemo(() => {
const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
const endIndex = startIndex + lotTablePagingController.pageSize;
return prepareLotTableData.slice(startIndex, endIndex);
}, [prepareLotTableData, lotTablePagingController]);

// 分页处理函数
const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => {
setLotTablePagingController(prev => ({
...prev,
pageNum: newPage,
}));
}, []);

const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
setLotTablePagingController({
pageNum: 0,
pageSize: newPageSize,
});
}, []);

// ✅ Handle QR code submission
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(`✅ QR Code verified for lot: ${lotNo}`);
// ✅ Store the required quantity before creating stock out line
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
// ✅ Create stock out line and wait for it to complete
await onCreateStockOutLine(selectedLotForQr.lotId);
// ✅ Close modal
setQrModalOpen(false);
setSelectedLotForQr(null);
// ✅ Set pick quantity AFTER stock out line creation and refresh is complete
if (selectedRowId) {
// Add a small delay to ensure the data refresh from onCreateStockOutLine is complete
setTimeout(() => {
onPickQtyChange(selectedRowId, lotId, requiredQty);
console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500); // 500ms delay to ensure refresh is complete
}
// ✅ Show success message
console.log("Stock out line created successfully!");
}
}, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]);

return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Lot#")}</TableCell>
<TableCell>{t("Lot Expiry Date")}</TableCell>
<TableCell>{t("Lot Location")}</TableCell>
<TableCell align="right">{t("Available Lot")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell>{t("Stock Unit")}</TableCell>
<TableCell align="center">{t("QR Code Scan")}</TableCell>
<TableCell align="center">{t("QC Check")}</TableCell>
<TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell>
<TableCell align="center">{t("Submit")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedLotTableData.length === 0 ? (
<TableRow>
<TableCell colSpan={11} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedLotTableData.map((lot, index) => (
<TableRow key={lot.id}>
<TableCell>
<Checkbox
checked={selectedLotRowId === `row_${index}`}
onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
// ✅ Allow selection of available AND insufficient_stock lots
//disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
value={`row_${index}`}
name="lot-selection"
/>
</TableCell>
<TableCell>
<Box>
<Typography>{lot.lotNo}</Typography>
{lot.lotAvailability !== 'available' && (
<Typography variant="caption" color="error" display="block">
({lot.lotAvailability === 'expired' ? 'Expired' :
lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
'Unavailable'})
</Typography>
)}
</Box>
</TableCell>
<TableCell>{lot.expiryDate}</TableCell>
<TableCell>{lot.location}</TableCell>
<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>
<TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell>
<TableCell>{lot.stockUnit}</TableCell>
{/* QR Code Scan Button */}
<TableCell align="center">
<Box sx={{ textAlign: 'center' }}>
<Button
variant="outlined"
size="small"
onClick={() => {
setSelectedLotForQr(lot);
setQrModalOpen(true);
resetScan();
}}
// ✅ Disable when:
// 1. Lot is expired or unavailable
// 2. Already scanned (has stockOutLineId)
// 3. Not selected (selectedLotRowId doesn't match)
disabled={
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
Boolean(lot.stockOutLineId) ||
selectedLotRowId !== `row_${index}`
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
whiteSpace: 'nowrap',
minWidth: '40px',
// ✅ Visual feedback
opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
}}
startIcon={<QrCodeIcon />}
title={
selectedLotRowId !== `row_${index}`
? "Please select this lot first to enable QR scanning"
: lot.stockOutLineId
? "Already scanned"
: "Click to scan QR code"
}
>
{lot.stockOutLineId ? t("Scanned") : t("Scan")}
</Button>
</Box>
</TableCell>
{/* QC Check Button */}
{/*
<TableCell align="center">
<Button
variant="outlined"
size="small"
onClick={() => {
if (selectedRowId && selectedRow) {
onQcCheck(selectedRow, selectedRow.pickOrderCode);
}
}}
// ✅ Enable QC check only when stock out line exists
disabled={!lot.stockOutLineId || selectedLotRowId !== `row_${index}`}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
whiteSpace: 'nowrap',
minWidth: '40px'
}}
>
{t("QC")}
</Button>
*/}
{/* Lot Actual Pick Qty */}
<TableCell align="right">
<TextField
type="number"
value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || '') : ''} // ✅ Fixed: Use empty string instead of 0
onChange={(e) => {
if (selectedRowId) {
const inputValue = e.target.value;
// ✅ Fixed: Handle empty string and prevent leading zeros
if (inputValue === '') {
// Allow empty input (user can backspace to clear)
onPickQtyChange(selectedRowId, lot.lotId, 0);
} else {
// Parse the number and prevent leading zeros
const numValue = parseInt(inputValue, 10);
if (!isNaN(numValue)) {
onPickQtyChange(selectedRowId, lot.lotId, numValue);
}
}
}
}}
onBlur={(e) => {
// ✅ Fixed: When input loses focus, ensure we have a valid number
if (selectedRowId) {
const currentValue = pickQtyData[selectedRowId]?.[lot.lotId];
if (currentValue === undefined || currentValue === null) {
// Set to 0 if no value
onPickQtyChange(selectedRowId, lot.lotId, 0);
}
}
}}
inputProps={{
min: 0,
max: lot.availableQty,
step: 1 // Allow only whole numbers
}}
// ✅ Allow input for available AND insufficient_stock lots
disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
sx={{ width: '80px' }}
placeholder="0" // Show placeholder instead of default value
/>
</TableCell>
<TableCell align="center">
<Button
variant="outlined"
size="small"
onClick={async () => {
if (selectedRowId && selectedRow && lot.stockOutLineId) {
try {
// ✅ Call updateStockOutLineStatus to reject the stock out line
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'rejected',
qty: 0
});
// ✅ Refresh data after rejection
if (onDataRefresh) {
await onDataRefresh();
}
} catch (error) {
console.error("Error rejecting lot:", error);
}
}
}}
// ✅ Only enable if stock out line exists
disabled={!lot.stockOutLineId}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
whiteSpace: 'nowrap',
minWidth: '40px'
}}
>
{t("Reject")}
</Button>
</TableCell>
{/* Submit Button */}
<TableCell align="center">
<Button
variant="contained"
onClick={() => {
if (selectedRowId) {
onSubmitPickQty(selectedRowId, lot.lotId);
}
}}
disabled={
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
!pickQtyData[selectedRowId!]?.[lot.lotId] ||
!lot.stockOutLineStatus || // Must have stock out line
!['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) // Only these statuses
}
// ✅ Allow submission for available AND insufficient_stock lots
sx={{
fontSize: '0.75rem',
py: 0.5,
minHeight: '28px'
}}
>
{t("Submit")}
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{/* ✅ Status Messages Display */}
{paginatedLotTableData.length > 0 && (
<Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
{paginatedLotTableData.map((lot, index) => (
<Box key={lot.id} sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary">
<strong>Lot {lot.lotNo}:</strong> {getStatusMessage(lot)}
</Typography>
</Box>
))}
</Box>
)}

<TablePagination
component="div"
count={prepareLotTableData.length}
page={lotTablePagingController.pageNum}
rowsPerPage={lotTablePagingController.pageSize}
onPageChange={handleLotTablePageChange}
onRowsPerPageChange={handleLotTablePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
{/* ✅ QR Code Modal */}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
setQrModalOpen(false);
setSelectedLotForQr(null);
stopScan();
resetScan();
}}
lot={selectedLotForQr}
onQrCodeSubmit={handleQrCodeSubmit}
/>
</>
);
};

export default LotTable;

+ 1
- 1
src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx Wyświetl plik

@@ -120,7 +120,7 @@ interface LotPickData {
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;


+ 1
- 1
src/components/FinishedGoodSearch/newcreatitem.tsx Wyświetl plik

@@ -687,7 +687,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
formProps.reset();
setHasSearched(false);
setFilteredItems([]);
alert(t("All pick orders created successfully"));
// alert(t("All pick orders created successfully"));
// 通知父组件切换到 Assign & Release 标签页
if (onPickOrderCreated) {


+ 222
- 184
src/components/PickOrderSearch/LotTable.tsx Wyświetl plik

@@ -20,11 +20,12 @@ import {
import { useCallback, useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import { GetPickOrderLineInfo, recordPickExecutionIssue } from "@/app/api/pickOrder/actions";
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions";
import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions"; // ✅ Add this import
import PickExecutionForm from "./PickExecutionForm";
interface LotPickData {
id: number;
lotId: number;
@@ -37,7 +38,10 @@ interface LotPickData {
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // ✅ 添加 'rejected'
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
@@ -52,7 +56,7 @@ interface PickQtyData {
interface LotTableProps {
lotData: LotPickData[];
selectedRowId: number | null;
selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // ✅ 添加 pickOrderId
pickQtyData: PickQtyData;
selectedLotRowId: string | null;
selectedLotId: number | null;
@@ -63,6 +67,9 @@ interface LotTableProps {
onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void;
onLotSelectForInput: (lot: LotPickData) => void;
showInputBody: boolean;
totalPickedByAllPickOrders: number;
outQty: number;
holdQty: number;
setShowInputBody: (show: boolean) => void;
selectedLotForInput: LotPickData | null;
generateInputBody: () => any;
@@ -383,7 +390,11 @@ const LotTable: React.FC<LotTableProps> = ({
onLotDataRefresh,
}) => {
const { t } = useTranslation("pickOrder");
const calculateRemainingRequiredQty = useCallback((lot: LotPickData) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
return Math.max(0, requiredQty - stockOutLineQty);
}, []);
// ✅ Add QR scanner context
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
@@ -414,7 +425,7 @@ const LotTable: React.FC<LotTableProps> = ({
case 'completed':
return t("Pick order completed successfully!");
case 'rejected':
return t("QC check failed. Lot has been rejected and marked as unavailable.");
return t("Lot has been rejected and marked as unavailable.");
case 'unavailable':
return t("This order is insufficient, please pick another lot.");
default:
@@ -455,7 +466,7 @@ const LotTable: React.FC<LotTableProps> = ({
if (!selectedRowId) return lot.availableQty;
const lactualPickQty = lot.actualPickQty || 0;
const actualPickQty = pickQtyData[selectedRowId]?.[lot.lotId] || 0;
const remainingQty = lot.inQty - actualPickQty - lactualPickQty;
const remainingQty = lot.inQty - lot.outQty;
// Ensure it doesn't go below 0
return Math.max(0, remainingQty);
@@ -490,6 +501,57 @@ const LotTable: React.FC<LotTableProps> = ({
}
}, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]);

// ✅ 添加 PickExecutionForm 相关的状态
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null);

// ✅ 添加处理函数
const handlePickExecutionForm = useCallback((lot: LotPickData) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
if (!lot) {
console.warn("No lot data provided for pick execution form");
return;
}
console.log("Opening pick execution form for lot:", lot.lotNo);
setSelectedLotForExecutionForm(lot);
setPickExecutionFormOpen(true);
console.log("Pick execution form opened for lot ID:", lot.lotId);
}, []);

const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
try {
console.log("Pick execution form submitted:", data);
// ✅ 调用 API 提交数据
const result = await recordPickExecutionIssue(data);
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log("✅ Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null);
// ✅ 刷新数据
if (onDataRefresh) {
await onDataRefresh();
}
if (onLotDataRefresh) {
await onLotDataRefresh();
}
} catch (error) {
console.error("Error submitting pick execution form:", error);
}
}, [onDataRefresh, onLotDataRefresh]);

return (
<>
<TableContainer component={Paper}>
@@ -526,24 +588,43 @@ const LotTable: React.FC<LotTableProps> = ({
</TableRow>
) : (
paginatedLotTableData.map((lot, index) => (
<TableRow key={lot.id}>
<TableRow
key={lot.id}
sx={{
backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
'& .MuiTableCell-root': {
color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
}
}}
>
<TableCell>
<Checkbox
checked={selectedLotRowId === `row_${index}`}
onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
// ✅ Allow selection of available AND insufficient_stock lots
//disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
value={`row_${index}`}
name="lot-selection"
/>
</TableCell>
<Checkbox
checked={selectedLotRowId === `row_${index}`}
onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
// ✅ 禁用 rejected、expired 和 status_unavailable 的批次
disabled={lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'} // ✅ 添加 rejected
value={`row_${index}`}
name="lot-selection"
/>
</TableCell>
<TableCell>
<Box>
<Typography>{lot.lotNo}</Typography>
<Typography
sx={{
color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
}}
>
{lot.lotNo}
</Typography>
{lot.lotAvailability !== 'available' && (
<Typography variant="caption" color="error" display="block">
({lot.lotAvailability === 'expired' ? 'Expired' :
lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
lot.lotAvailability === 'rejected' ? 'Rejected' : // ✅ 添加 rejected 显示
'Unavailable'})
</Typography>
)}
@@ -552,174 +633,112 @@ const LotTable: React.FC<LotTableProps> = ({
<TableCell>{lot.expiryDate}</TableCell>
<TableCell>{lot.location}</TableCell>
<TableCell>{lot.stockUnit}</TableCell>
<TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell>
<TableCell align="right">{lot.inQty.toLocaleString()??'0'}</TableCell>
<TableCell align="center">
{/* Show QR Scan Button if not scanned, otherwise show TextField */}
{!lot.stockOutLineId ? (
<Button
variant="outlined"
size="small"
onClick={() => {
setSelectedLotForQr(lot);
setQrModalOpen(true);
resetScan();
}}
// ✅ Disable when:
// 1. Lot is expired or unavailable
// 2. Not selected (selectedLotRowId doesn't match)
disabled={
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
selectedLotRowId !== `row_${index}`
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '40px', // ✅ Match TextField height
whiteSpace: 'nowrap',
minWidth: '80px', // ✅ Match TextField width
// ✅ Visual feedback
opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
}}
startIcon={<QrCodeIcon />}
title={
selectedLotRowId !== `row_${index}`
? "Please select this lot first to enable QR scanning"
: "Click to scan QR code"
}
>
{t("Scan")}
</Button>
) : (
<TextField
type="number"
value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || '') : ''}
onChange={(e) => {
if (selectedRowId) {
const inputValue = e.target.value;
// ✅ Fixed: Handle empty string and prevent leading zeros
if (inputValue === '') {
// Allow empty input (user can backspace to clear)
onPickQtyChange(selectedRowId, lot.lotId, 0);
} else {
// Parse the number and prevent leading zeros
const numValue = parseInt(inputValue, 10);
if (!isNaN(numValue)) {
onPickQtyChange(selectedRowId, lot.lotId, numValue);
}
}
}
}}
onBlur={(e) => {
// ✅ Fixed: When input loses focus, ensure we have a valid number
if (selectedRowId) {
const currentValue = pickQtyData[selectedRowId]?.[lot.lotId];
if (currentValue === undefined || currentValue === null) {
// Set to 0 if no value
onPickQtyChange(selectedRowId, lot.lotId, 0);
}
}
}}
inputProps={{
min: 0,
max: lot.availableQty,
step: 1 // Allow only whole numbers
}}
// ✅ Allow input for available AND insufficient_stock lots
disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'}
sx={{
width: '80px',
'& .MuiInputBase-root': {
height: '40px', // ✅ Match table cell height
},
'& .MuiInputBase-input': {
height: '40px',
padding: '8px 12px', // ✅ Adjust padding to center text vertically
}
}}
placeholder="0" // Show placeholder instead of default value
/>
)}
</TableCell>
{/*<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>*/}
<TableCell align="right">{calculateRemainingAvailableQty(lot).toLocaleString()}</TableCell>
{/* <TableCell>{lot.stockUnit}</TableCell> */}
<TableCell align="right">{calculateRemainingRequiredQty(lot).toLocaleString()}</TableCell>
<TableCell align="right">
{(() => {
const inQty = lot.inQty || 0;
const outQty = lot.outQty || 0;
{/* QR Code Scan Button */}
{/*
<TableCell align="center">
<Box sx={{ textAlign: 'center' }}>
<Button
variant="outlined"
size="small"
onClick={() => {
setSelectedLotForQr(lot);
setQrModalOpen(true);
resetScan();
}}
// ✅ Disable when:
// 1. Lot is expired or unavailable
// 2. Already scanned (has stockOutLineId)
// 3. Not selected (selectedLotRowId doesn't match)
disabled={
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
Boolean(lot.stockOutLineId) ||
selectedLotRowId !== `row_${index}`
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
whiteSpace: 'nowrap',
minWidth: '40px',
// ✅ Visual feedback
opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
}}
startIcon={<QrCodeIcon />}
title={
selectedLotRowId !== `row_${index}`
? "Please select this lot first to enable QR scanning"
: lot.stockOutLineId
? "Already scanned"
: "Click to scan QR code"
}
>
{lot.stockOutLineId ? t("Scanned") : t("Scan")}
</Button>
</Box>
const result = inQty - outQty;
return result.toLocaleString();
})()}
</TableCell>
*/}
{/* QC Check Button */}
{/*
<TableCell align="center">
<Button
variant="outlined"
size="small"
onClick={() => {
if (selectedRowId && selectedRow) {
onQcCheck(selectedRow, selectedRow.pickOrderCode);
}
}}
// ✅ Enable QC check only when stock out line exists
disabled={!lot.stockOutLineId || selectedLotRowId !== `row_${index}`}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
whiteSpace: 'nowrap',
minWidth: '40px'
}}
>
{t("QC")}
</Button>
*/}
{/* Lot Actual Pick Qty */}
{/* Show QR Scan Button if not scanned, otherwise show TextField + Pick Form */}
{!lot.stockOutLineId ? (
<Button
variant="outlined"
size="small"
onClick={() => {
setSelectedLotForQr(lot);
setQrModalOpen(true);
resetScan();
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
selectedLotRowId !== `row_${index}`
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '40px',
whiteSpace: 'nowrap',
minWidth: '80px',
opacity: selectedLotRowId === `row_${index}` ? 1 : 0.5
}}
startIcon={<QrCodeIcon />}
title={
selectedLotRowId !== `row_${index}`
? "Please select this lot first to enable QR scanning"
: "Click to scan QR code"
}
>
{t("Scan")}
</Button>
) : (
// ✅ 当有 stockOutLineId 时,显示 TextField + Pick Form 按钮
<Stack direction="row" spacing={1} alignItems="center">
{/* ✅ 恢复 TextField 用于正常数量输入 */}
<TextField
type="number"
size="small"
value={pickQtyData[selectedRowId!]?.[lot.lotId] || ''}
onChange={(e) => {
if (selectedRowId) {
onPickQtyChange(selectedRowId, lot.lotId, parseFloat(e.target.value) || 0);
}
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
selectedLotRowId !== `row_${index}` ||
lot.stockOutLineStatus === 'completed' // ✅ 完成时禁用输入
}
inputProps={{
min: 0,
max: calculateRemainingRequiredQty(lot),
step: 0.01
}}
sx={{
width: '80px',
'& .MuiInputBase-input': {
fontSize: '0.75rem',
textAlign: 'center',
padding: '8px 4px'
}
}}
placeholder="0"
/>
{/* ✅ 添加 Pick Form 按钮用于问题情况 */}
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
}}
title="Report missing or bad items"
>
{t("Issue")}
</Button>
</Stack>
)}
</TableCell>
{/*<TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell>*/}
<TableCell align="right">{calculateRemainingAvailableQty(lot).toLocaleString()}</TableCell>

<TableCell align="center">
{/*
<Stack direction="column" spacing={1} alignItems="center">
<Button
variant="outlined"
@@ -759,6 +778,7 @@ const LotTable: React.FC<LotTableProps> = ({
{t("Reject")}
</Button>
</Stack>
*/}
{/*}
</TableCell>
@@ -774,10 +794,12 @@ const LotTable: React.FC<LotTableProps> = ({
}}
disabled={
(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') ||
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') || // ✅ 添加 rejected
!pickQtyData[selectedRowId!]?.[lot.lotId] ||
!lot.stockOutLineStatus || // Must have stock out line
!['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase()) // Only these statuses
!lot.stockOutLineStatus ||
!['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase())
}
// ✅ Allow submission for available AND insufficient_stock lots
sx={{
@@ -838,6 +860,22 @@ const LotTable: React.FC<LotTableProps> = ({
lot={selectedLotForQr}
onQrCodeSubmit={handleQrCodeSubmit}
/>

{/* ✅ Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && (
<PickExecutionForm
open={pickExecutionFormOpen}
onClose={() => {
setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null);
}}
onSubmit={handlePickExecutionFormSubmit}
selectedLot={selectedLotForExecutionForm}
selectedPickOrderLine={selectedRow}
pickOrderId={selectedRow.pickOrderId}
pickOrderCreateDate={new Date()}
/>
)}
</>
);
};


+ 186
- 271
src/components/PickOrderSearch/PickExecution.tsx Wyświetl plik

@@ -46,6 +46,7 @@ import {
createStockOutLine,
updateStockOutLineStatus,
resuggestPickOrder,
checkAndCompletePickOrderByConsoCode,
} from "@/app/api/pickOrder/actions";
import { EditNote } from "@mui/icons-material";
import { fetchNameList, NameList } from "@/app/api/user/actions";
@@ -69,9 +70,10 @@ import dayjs from "dayjs";
import { dummyQCData } from "../PoDetail/dummyQcTemplate";
import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
import LotTable from './LotTable';
import PickOrderDetailsTable from './PickOrderDetailsTable'; // ✅ Import the new component
import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
import { useSession } from "next-auth/react"; // ✅ Add session import
import { SessionWithTokens } from "@/config/authConfig"; // ✅ Add custom session type
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";

interface Props {
filterArgs: Record<string, any>;
@@ -85,11 +87,14 @@ interface LotPickData {
location: string;
stockUnit: string;
inQty: number;
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
@@ -104,9 +109,8 @@ interface PickQtyData {
const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Add session
const { data: session } = useSession() as { data: SessionWithTokens | null };
// ✅ Get current user ID from session with proper typing
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const [filteredPickOrders, setFilteredPickOrders] = useState(
@@ -141,11 +145,10 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
} | null>(null);
const [selectedLotForQc, setSelectedLotForQc] = useState<LotPickData | null>(null);

// ✅ Add lot selection state variables
const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null);
const [selectedLotId, setSelectedLotId] = useState<number | null>(null);

// 新增:分页控制器
// ✅ Keep only the main table paging controller
const [mainTablePagingController, setMainTablePagingController] = useState({
pageNum: 0,
pageSize: 10,
@@ -155,7 +158,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
pageSize: 10,
});

// Add missing search state variables
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [originalPickOrderData, setOriginalPickOrderData] = useState<GetPickOrderInfoResponse | null>(null);

@@ -199,6 +201,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
useEffect(() => {
fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs);
}, [fetchNewPageConsoPickOrder, filterArgs]);

const handleUpdateStockOutLineStatus = useCallback(async (
stockOutLineId: number,
status: string,
@@ -217,16 +220,15 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
if (result) {
console.log("Stock out line status updated successfully:", result);
// Refresh lot data to show updated status
if (selectedRowId) {
handleRowSelect(selectedRowId);
}
}
} catch (error) {
console.error("Error updating stock out line status:", error);
}
}, [selectedRowId]);

const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => {
let isReleasable = true;
for (const item of itemList) {
@@ -260,26 +262,52 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const handleFetchAllPickOrderDetails = useCallback(async () => {
setDetailLoading(true);
try {
// ✅ Use current user ID for filtering
const data = await fetchAllPickOrderDetails(currentUserId);
setPickOrderDetails(data);
setOriginalPickOrderData(data); // Store original data for filtering
console.log("All Pick Order Details for user:", currentUserId, data);
const initialPickQtyData: PickQtyData = {};
data.pickOrders.forEach((pickOrder: any) => {
pickOrder.pickOrderLines.forEach((line: any) => {
initialPickQtyData[line.id] = {};
if (data && data.pickOrders) {
setPickOrderDetails(data);
setOriginalPickOrderData(data);
const initialPickQtyData: PickQtyData = {};
data.pickOrders.forEach((pickOrder: any) => {
pickOrder.pickOrderLines.forEach((line: any) => {
initialPickQtyData[line.id] = {};
});
});
});
setPickQtyData(initialPickQtyData);
setPickQtyData(initialPickQtyData);
} else {
console.log("No pick order data returned");
setPickOrderDetails({
consoCode: null,
pickOrders: [],
items: []
});
setOriginalPickOrderData({
consoCode: null,
pickOrders: [],
items: []
});
setPickQtyData({});
}
} catch (error) {
console.error("Error fetching all pick order details:", error);
setPickOrderDetails({
consoCode: null,
pickOrders: [],
items: []
});
setOriginalPickOrderData({
consoCode: null,
pickOrders: [],
items: []
});
setPickQtyData({});
} finally {
setDetailLoading(false);
}
}, [currentUserId]); // ✅ Add currentUserId as dependency
}, [currentUserId]);

useEffect(() => {
handleFetchAllPickOrderDetails();
@@ -331,7 +359,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number | string) => {
console.log("Changing pick qty:", { lineId, lotId, value });
// ✅ Handle both number and string values
const numericValue = typeof value === 'string' ? (value === '' ? 0 : parseInt(value, 10)) : value;
setPickQtyData(prev => {
@@ -351,24 +378,28 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const qty = pickQtyData[lineId]?.[lotId] || 0;
console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`);
// ✅ Find the stock out line for this lot
const selectedLot = lotData.find(lot => lot.lotId === lotId);
if (!selectedLot?.stockOutLineId) {
return;
}
try {
// ✅ Only two statuses: partially_completed or completed
let newStatus = 'partially_completed'; // Default status
// ✅ FIXED: 计算累计拣货数量
const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty;
console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0);
console.log("🔍 DEBUG - Current submit:", qty);
console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot);
console.log("�� DEBUG - Required qty:", selectedLot.requiredQty);
if (qty >= selectedLot.requiredQty) {
newStatus = 'completed'; // Full quantity picked
// ✅ FIXED: 状态应该基于累计拣货数量
let newStatus = 'partially_completed';
if (totalPickedForThisLot >= selectedLot.requiredQty) {
newStatus = 'completed';
}
// If qty < requiredQty, stays as 'partially_completed'
// ✅ Function 1: Update stock out line with new status and quantity
console.log("�� DEBUG - Calculated status:", newStatus);
try {
// ✅ Function 1: Update stock out line with new status and quantity
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLot.stockOutLineId,
status: newStatus,
@@ -379,10 +410,9 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
} catch (error) {
console.error("❌ Error updating stock out line:", error);
return; // Stop execution if this fails
return;
}
// ✅ Function 2: Update inventory lot line (balance hold_qty and out_qty)
if (qty > 0) {
const inventoryLotLineUpdate = await updateInventoryLotLineQuantities({
inventoryLotLineId: lotId,
@@ -394,26 +424,74 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Inventory lot line updated:", inventoryLotLineUpdate);
}
// ✅ Function 3: Handle inventory table onhold if needed
// ✅ RE-ENABLE: Check if pick order should be completed
if (newStatus === 'completed') {
// All required quantity picked - might need to update inventory status
// Note: We'll handle inventory update in a separate function or after selectedRow is available
console.log("Completed status - inventory update needed but selectedRow not available yet");
console.log("✅ Stock out line completed, checking if entire pick order is complete...");
// ✅ 添加调试日志来查看所有 pick orders 的 consoCode
console.log("📋 DEBUG - All pick orders and their consoCodes:");
if (pickOrderDetails) {
pickOrderDetails.pickOrders.forEach((pickOrder, index) => {
console.log(` Pick Order ${index + 1}: ID=${pickOrder.id}, Code=${pickOrder.code}, ConsoCode=${pickOrder.consoCode}`);
});
}
// ✅ FIXED: 直接查找 consoCode,不依赖 selectedRow
if (pickOrderDetails) {
let currentConsoCode: string | null = null;
// 找到当前选中行所属的 pick order
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
// ✅ 直接使用 pickOrder.code 作为 consoCode
currentConsoCode = pickOrder.consoCode;
console.log(`�� DEBUG - Found consoCode for line ${selectedRowId}: ${currentConsoCode} (from pick order ${pickOrder.id})`);
break;
}
}
if (currentConsoCode) {
try {
console.log(`🔍 Checking completion for consoCode: ${currentConsoCode}`);
const completionResponse = await checkAndCompletePickOrderByConsoCode(currentConsoCode);
console.log("�� Completion response:", completionResponse);
if (completionResponse.message === "completed") {
console.log("🎉 Pick order completed successfully!");
await handleFetchAllPickOrderDetails();
// 刷新当前选中的行数据
if (selectedRowId) {
await handleRowSelect(selectedRowId, true);
}
} else if (completionResponse.message === "not completed") {
console.log("⏳ Pick order not completed yet, more lines remaining");
} else {
console.error("❌ Error checking completion:", completionResponse.message);
}
} catch (error) {
console.error("❌ Error checking pick order completion:", error);
}
} else {
console.warn("⚠️ No consoCode found for current pick order, cannot check completion");
}
}
}
console.log("All updates completed successfully");
// ✅ Refresh lot data to show updated quantities
if (selectedRowId) {
await handleRowSelect(selectedRowId, true);
// Note: We'll handle refresh after the function is properly defined
console.log("Data refresh needed but handleRowSelect not available yet");
}
await handleFetchAllPickOrderDetails();
} catch (error) {
console.error("Error updating pick quantity:", error);
}
}, [pickQtyData, lotData, selectedRowId]);
}, [pickQtyData, lotData, selectedRowId, pickOrderDetails, handleFetchAllPickOrderDetails]);

const getTotalPickedQty = useCallback((lineId: number) => {
const lineData = pickQtyData[lineId];
@@ -421,30 +499,22 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
return Object.values(lineData).reduce((sum, qty) => sum + qty, 0);
}, [pickQtyData]);



const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => {
// ✅ Get the selected lot for QC
if (!selectedLotId) {
return;
}
const selectedLot = lotData.find(lot => lot.lotId === selectedLotId);
if (!selectedLot) {
//alert("Selected lot not found in lot data");
return;
}
// ✅ Check if stock out line exists
if (!selectedLot.stockOutLineId) {
//alert("Please create a stock out line first before performing QC check");
return;
}
setSelectedLotForQc(selectedLot);
// ✅ ALWAYS use dummy data for consistent behavior
const transformedDummyData = dummyQCData.map(item => ({
id: item.id,
code: item.code,
@@ -453,7 +523,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
lowerLimit: undefined,
upperLimit: undefined,
description: item.qcDescription,
// ✅ Always reset QC result properties to undefined for fresh start
qcPassed: undefined,
failQty: undefined,
remarks: undefined
@@ -461,7 +530,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setQcItems(transformedDummyData as QcItemWithChecks[]);
// ✅ Get existing QC results if any (for display purposes only)
let qcResult: any[] = [];
try {
const rawQcResult = await fetchPickOrderQcResult(line.id);
@@ -491,7 +559,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setSelectedItemForQc(item);
}, []);

// 新增:处理分页变化
// ✅ Main table pagination handlers
const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => {
setMainTablePagingController(prev => ({
...prev,
@@ -522,31 +590,27 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}, []);

// ✅ Fix lot selection logic
const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => {
console.log("=== DEBUG: Lot Selection ===");
console.log("uniqueLotId:", uniqueLotId);
console.log("lotId (inventory lot line ID):", lotId);
// Find the selected lot data
const selectedLot = lotData.find(lot => lot.lotId === lotId);
console.log("Selected lot data:", selectedLot);
// If clicking the same lot, unselect it
if (selectedLotRowId === uniqueLotId) {
setSelectedLotRowId(null);
setSelectedLotId(null);
} else {
// Select the new lot
setSelectedLotRowId(uniqueLotId);
setSelectedLotId(lotId);
}
}, [selectedLotRowId]);

// ✅ Add function to handle row selection that resets lot selection
const handleRowSelect = useCallback(async (lineId: number, preserveLotSelection: boolean = false) => {
setSelectedRowId(lineId);
// ✅ Only reset lot selection if not preserving
if (!preserveLotSelection) {
setSelectedLotRowId(null);
setSelectedLotId(null);
@@ -557,13 +621,16 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Lot details from API:", lotDetails);
const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({
id: lot.id, // This should be the unique row ID for the table
lotId: lot.lotId, // This is the inventory lot line ID
id: lot.id,
lotId: lot.lotId,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A',
location: lot.location,
stockUnit: lot.stockUnit,
inQty: lot.inQty,
outQty: lot.outQty,
holdQty: lot.holdQty,
totalPickedByAllPickOrders: lot.totalPickedByAllPickOrders,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty || 0,
@@ -581,33 +648,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, []);

const prepareMainTableData = useMemo(() => {
if (!pickOrderDetails) return [];
return pickOrderDetails.pickOrders.flatMap((pickOrder) =>
pickOrder.pickOrderLines.map((line) => {
// 修复:处理 availableQty 可能为 null 的情况
const availableQty = line.availableQty ?? 0;
const balanceToPick = availableQty - line.requiredQty;
// ✅ 使用 dayjs 进行一致的日期格式化
const formattedTargetDate = pickOrder.targetDate
? dayjs(pickOrder.targetDate).format('YYYY-MM-DD')
: 'N/A';
return {
...line,
pickOrderCode: pickOrder.code,
targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期
balanceToPick: balanceToPick,
pickedQty: line.pickedQty,
// 确保 availableQty 不为 null
availableQty: availableQty,
};
})
);
}, [pickOrderDetails]);

const prepareLotTableData = useMemo(() => {
return lotData.map((lot) => ({
...lot,
@@ -615,13 +655,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}));
}, [lotData]);

// 新增:分页数据
const paginatedMainTableData = useMemo(() => {
const startIndex = mainTablePagingController.pageNum * mainTablePagingController.pageSize;
const endIndex = startIndex + mainTablePagingController.pageSize;
return prepareMainTableData.slice(startIndex, endIndex);
}, [prepareMainTableData, mainTablePagingController]);

const paginatedLotTableData = useMemo(() => {
const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize;
const endIndex = startIndex + lotTablePagingController.pageSize;
@@ -634,11 +667,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
return { ...foundLine, pickOrderCode: pickOrder.code };
return { ...foundLine, pickOrderCode: pickOrder.code,
pickOrderId: pickOrder.id };
}
}
return null;
}, [selectedRowId, pickOrderDetails]);

const handleInventoryUpdate = useCallback(async (itemId: number, lotId: number, qty: number) => {
try {
const inventoryUpdate = await updateInventoryStatus({
@@ -653,16 +688,17 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.error("Error updating inventory status:", error);
}
}, []);

const handleLotDataRefresh = useCallback(async () => {
if (selectedRowId) {
try {
await handleRowSelect(selectedRowId, true); // Preserve lot selection
await handleRowSelect(selectedRowId, true);
} catch (error) {
console.error("Error refreshing lot data:", error);
}
}
}, [selectedRowId, handleRowSelect]);
// ✅ Add this function after handleRowSelect is defined
const handleDataRefresh = useCallback(async () => {
if (selectedRowId) {
try {
@@ -672,15 +708,14 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}
}, [selectedRowId, handleRowSelect]);

const handleInsufficientStock = useCallback(async () => {
console.log("Insufficient stock - testing resuggest API");
if (!selectedRowId || !pickOrderDetails) {
// alert("Please select a pick order line first");
return;
}
// Find the pick order ID from the selected row
let pickOrderId: number | null = null;
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
@@ -691,55 +726,41 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
if (!pickOrderId) {
// alert("Could not find pick order ID for selected line");
return;
}
try {
console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`);
// Call the resuggest API
const result = await resuggestPickOrder(pickOrderId);
console.log("Resuggest API result:", result);
if (result.code === "SUCCESS") {
//alert(`✅ Resuggest successful!\n\nMessage: ${result.message}\n\nRemoved: ${result.message?.includes('Removed') ? 'Yes' : 'No'}\nCreated: ${result.message?.includes('created') ? 'Yes' : 'No'}`);
// Refresh the lot data to show the new suggestions
if (selectedRowId) {
await handleRowSelect(selectedRowId);
}
// Also refresh the main pick order details
await handleFetchAllPickOrderDetails();
} else {
//alert(`❌ Resuggest failed!\n\nError: ${result.message}`);
}
} catch (error) {
console.error("Error calling resuggest API:", error);
//alert(`❌ Error calling resuggest API:\n\n${error instanceof Error ? error.message : 'Unknown error'}`);
}
}, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]);

// Add this function (around line 350)
const hasSelectedLots = useCallback((lineId: number) => {
return selectedLotRowId !== null;
}, [selectedLotRowId]);

// Add state for showing input body
const [showInputBody, setShowInputBody] = useState(false);
const [selectedLotForInput, setSelectedLotForInput] = useState<LotPickData | null>(null);

// Add function to handle lot selection for input body display
const handleLotSelectForInput = useCallback((lot: LotPickData) => {
setSelectedLotForInput(lot);
setShowInputBody(true);
}, []);

// Add function to generate input body
const generateInputBody = useCallback((): CreateStockOutLine | null => {
if (!selectedLotForInput || !selectedRowId || !selectedRow || !pickOrderDetails?.consoCode) {
return null;
@@ -753,9 +774,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
};
}, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]);



// Add function to handle create stock out line
const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => {
if (!selectedRowId || !pickOrderDetails?.consoCode) {
console.error("Missing required data for creating stock out line.");
@@ -763,12 +781,21 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
try {
// ✅ Store current lot selection before refresh
const currentSelectedLotRowId = selectedLotRowId;
const currentSelectedLotId = selectedLotId;
let correctConsoCode: string | null = null;
if (pickOrderDetails && selectedRowId) {
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
correctConsoCode = pickOrder.consoCode;
console.log(`🔍 Found consoCode for line ${selectedRowId}: ${correctConsoCode} (from pick order ${pickOrder.id})`);
break;
}
}
}
const stockOutLineData: CreateStockOutLine = {
consoCode: pickOrderDetails.consoCode,
consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // ✅ 使用正确的 consoCode
pickOrderLineId: selectedRowId,
inventoryLotLineId: inventoryLotLineId,
qty: 0.0
@@ -777,7 +804,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("=== STOCK OUT LINE CREATION DEBUG ===");
console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2));
// ✅ Use the correct API function
const result = await createStockOutLine(stockOutLineData);
console.log("Stock Out Line created:", result);
@@ -785,16 +811,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
if (result) {
console.log("Stock out line created successfully:", result);
// ✅ Auto-refresh data after successful creation
console.log("🔄 Refreshing data after stock out line creation...");
try {
// ✅ Refresh lot data for the selected row (maintains selection)
if (selectedRowId) {
await handleRowSelect(selectedRowId, true); // ✅ Preserve lot selection
await handleRowSelect(selectedRowId, true);
}
// ✅ Refresh main pick order details
await handleFetchAllPickOrderDetails();
console.log("✅ Data refresh completed - lot selection maintained!");
@@ -802,7 +825,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.error("❌ Error refreshing data:", refreshError);
}
setShowInputBody(false); // Hide preview after successful creation
setShowInputBody(false);
} else {
console.error("Failed to create stock out line: No response");
}
@@ -811,22 +834,17 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [selectedRowId, pickOrderDetails?.consoCode, handleRowSelect, handleFetchAllPickOrderDetails, selectedLotRowId, selectedLotId]);

// ✅ New function to refresh data while preserving lot selection
const handleRefreshDataPreserveSelection = useCallback(async () => {
if (!selectedRowId) return;
// ✅ Store current lot selection
const currentSelectedLotRowId = selectedLotRowId;
const currentSelectedLotId = selectedLotId;
try {
// ✅ Refresh lot data
await handleRowSelect(selectedRowId, true); // ✅ Preserve selection
await handleRowSelect(selectedRowId, true);
// ✅ Refresh main pick order details
await handleFetchAllPickOrderDetails();
// ✅ Restore lot selection
setSelectedLotRowId(currentSelectedLotRowId);
setSelectedLotId(currentSelectedLotId);
@@ -836,106 +854,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]);

// 自定义主表格组件
const CustomMainTable = () => {
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right">{t("Order Quantity")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Qty Already Picked")}</TableCell>
<TableCell align="right">{t("Stock Unit")}</TableCell>
<TableCell align="right">{t("Target Date")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedMainTableData.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedMainTableData.map((line) => {
// 修复:处理 availableQty 可能为 null 的情况,并确保负值显示为 0
const availableQty = line.availableQty ?? 0;
const balanceToPick = Math.max(0, availableQty - line.requiredQty); // 确保不为负数
const totalPickedQty = getTotalPickedQty(line.id);
const actualPickedQty = line.pickedQty ?? 0;
return (
<TableRow
key={line.id}
sx={{
"& > *": { borderBottom: "unset" },
color: "black",
backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit",
cursor: "pointer",
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
<TableCell align="center" sx={{ width: "60px" }}>
<Checkbox
checked={selectedRowId === line.id}
onChange={(e) => {
if (e.target.checked) {
handleRowSelect(line.id);
} else {
setSelectedRowId(null);
setLotData([]);
}
}}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell align="left">{line.pickOrderCode}</TableCell>
<TableCell align="left">{line.itemCode}</TableCell>
<TableCell align="left">{line.itemName}</TableCell>
<TableCell align="right">{line.requiredQty}</TableCell>
<TableCell align="right" sx={{
color: availableQty >= line.requiredQty ? 'success.main' : 'error.main',
}}>
{availableQty.toLocaleString()} {/* 添加千位分隔符 */}
</TableCell>
<TableCell align="right">{actualPickedQty}</TableCell>
<TableCell align="right">{line.uomDesc}</TableCell>
<TableCell align="right">{line.targetDate}</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={prepareMainTableData.length}
page={mainTablePagingController.pageNum}
rowsPerPage={mainTablePagingController.pageSize}
onPageChange={handleMainTablePageChange}
onRowsPerPageChange={handleMainTablePageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

// Add search criteria
// ✅ Search criteria
const searchCriteria: Criterion<any>[] = useMemo(
() => [
{
@@ -963,7 +882,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
[t],
);

// Add search handler
// ✅ Search handler
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);
@@ -971,7 +890,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
if (!originalPickOrderData) return;

const filtered = originalPickOrderData.pickOrders.filter((pickOrder) => {
// Check if any line in this pick order matches the search criteria
return pickOrder.pickOrderLines.some((line) => {
const itemCodeMatch = !query.itemCode ||
line.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
@@ -982,12 +900,10 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const pickOrderCodeMatch = !query.pickOrderCode ||
pickOrder.code?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
return itemCodeMatch && itemNameMatch && pickOrderCodeMatch ;
return itemCodeMatch && itemNameMatch && pickOrderCodeMatch;
});
});

// Create filtered data structure
const filteredData: GetPickOrderInfoResponse = {
...originalPickOrderData,
pickOrders: filtered
@@ -997,7 +913,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Filtered pick orders count:", filtered.length);
}, [originalPickOrderData, t]);

// Add reset handler
// ✅ Reset handler
const handleReset = useCallback(() => {
setSearchQuery({});
if (originalPickOrderData) {
@@ -1005,7 +921,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [originalPickOrderData]);

// Add this to debug the lot data
// ✅ Debug the lot data
useEffect(() => {
console.log("Lot data:", lotData);
console.log("Pick Qty Data:", pickQtyData);
@@ -1023,56 +939,55 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
/>
</Box>

{/* 主表格 */}
{/* ✅ Main table using the new component */}
<Box>
<Typography variant="h6" gutterBottom>
{t("Pick Order Details")}
</Typography>
{detailLoading ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<CircularProgress size={40} />
</Box>
) : pickOrderDetails ? (
<CustomMainTable />
) : (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<Typography variant="body2" color="text.secondary">
{t("Loading data...")}
</Typography>
</Box>
)}
<PickOrderDetailsTable
pickOrderDetails={pickOrderDetails}
detailLoading={detailLoading}
selectedRowId={selectedRowId}
onRowSelect={handleRowSelect}
onPageChange={handleMainTablePageChange}
onPageSizeChange={handleMainTablePageSizeChange}
pageNum={mainTablePagingController.pageNum}
pageSize={mainTablePagingController.pageSize}
/>
</Box>

{/* 批次表格 - 放在主表格下方 */}
{/* Lot table - below main table */}
{selectedRow && (
<Box>
<Typography variant="h6" gutterBottom>
{t("Item lot to be Pick:")} {selectedRow.pickOrderCode} - {selectedRow.itemName}
</Typography>
{/* 检查是否有可用的批次数据 */}
{lotData.length > 0 ? (
<LotTable
lotData={lotData}
selectedRowId={selectedRowId}
selectedRow={selectedRow}
pickQtyData={pickQtyData}
selectedLotRowId={selectedLotRowId}
selectedLotId={selectedLotId}
onLotSelection={handleLotSelection}
onPickQtyChange={handlePickQtyChange}
onSubmitPickQty={handleSubmitPickQty}
onCreateStockOutLine={handleCreateStockOutLine}
onQcCheck={handleQcCheck}
onDataRefresh={handleFetchAllPickOrderDetails}
onLotDataRefresh={handleLotDataRefresh}
onLotSelectForInput={handleLotSelectForInput}
showInputBody={showInputBody}
setShowInputBody={setShowInputBody}
selectedLotForInput={selectedLotForInput}
generateInputBody={generateInputBody}
/>
lotData={lotData}
selectedRowId={selectedRowId}
selectedRow={selectedRow}
pickQtyData={pickQtyData}
selectedLotRowId={selectedLotRowId}
selectedLotId={selectedLotId}
onLotSelection={handleLotSelection}
onPickQtyChange={handlePickQtyChange}
onSubmitPickQty={handleSubmitPickQty}
onCreateStockOutLine={handleCreateStockOutLine}
onQcCheck={handleQcCheck}
onDataRefresh={handleFetchAllPickOrderDetails}
onLotDataRefresh={handleLotDataRefresh}
onLotSelectForInput={handleLotSelectForInput}
showInputBody={showInputBody}
setShowInputBody={setShowInputBody}
selectedLotForInput={selectedLotForInput}
generateInputBody={generateInputBody}
// ✅ Add missing props
totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed
outQty={0} // You can calculate this from lotData if needed
holdQty={0} // You can calculate this from lotData if needed
/>
) : (
<Box
sx={{


+ 372
- 0
src/components/PickOrderSearch/PickExecutionForm.tsx Wyświetl plik

@@ -0,0 +1,372 @@
// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
"use client";

import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
Grid,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
import { fetchEscalationCombo } from "@/app/api/user/actions";

interface LotPickData {
id: number;
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
stockUnit: string;
inQty: number;
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
}

interface PickExecutionFormProps {
open: boolean;
onClose: () => void;
onSubmit: (data: PickExecutionIssueData) => Promise<void>;
selectedLot: LotPickData | null;
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// ✅ Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}

// 定义错误类型
interface FormErrors {
actualPickQty?: string;
missQty?: string;
badItemQty?: string;
issueRemark?: string;
handledBy?: string;
}

const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
open,
onClose,
onSubmit,
selectedLot,
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// ✅ Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
const [errors, setErrors] = useState<FormErrors>({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);

// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
return Math.max(0, remainingQty);
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
const requiredQty = lot.requiredQty-(lot.actualPickQty||0);
return Math.max(0, requiredQty);
}, []);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
try {
const escalationCombo = await fetchEscalationCombo();
setHandlers(escalationCombo);
} catch (error) {
console.error("Error fetching handlers:", error);
}
};
fetchHandlers();
}, []);

// 初始化表单数据 - 每次打开时都重新初始化
useEffect(() => {
if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
const getSafeDate = (dateValue: any): string => {
if (!dateValue) return new Date().toISOString().split('T')[0];
try {
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
return new Date().toISOString().split('T')[0];
}
return date.toISOString().split('T')[0];
} catch {
return new Date().toISOString().split('T')[0];
}
};

// 计算剩余可用数量
const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
console.log("=== PickExecutionForm Debug ===");
console.log("selectedLot:", selectedLot);
console.log("inQty:", selectedLot.inQty);
console.log("outQty:", selectedLot.outQty);
console.log("holdQty:", selectedLot.holdQty);
console.log("availableQty:", selectedLot.availableQty);
console.log("calculated remainingAvailableQty:", remainingAvailableQty);
console.log("=== End Debug ===");
setFormData({
pickOrderId: pickOrderId,
pickOrderCode: selectedPickOrderLine.pickOrderCode,
pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
pickExecutionDate: new Date().toISOString().split('T')[0],
pickOrderLineId: selectedPickOrderLine.id,
itemId: selectedPickOrderLine.itemId,
itemCode: selectedPickOrderLine.itemCode,
itemDescription: selectedPickOrderLine.itemName,
lotId: selectedLot.lotId,
lotNo: selectedLot.lotNo,
storeLocation: selectedLot.location,
requiredQty: selectedLot.requiredQty,
actualPickQty: selectedLot.actualPickQty || 0,
missQty: 0,
badItemQty: 0, // 初始化为 0,用户需要手动输入
issueRemark: '',
pickerName: '',
handledBy: undefined,
});
}
}, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除错误
if (errors[field as keyof FormErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
}, [errors]);

// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired');
}
// ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems');
newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems');
}
if (formData.missQty && formData.missQty < 0) {
newErrors.missQty = t('pickOrder.validation.missQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty < 0) {
newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) {
newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired');
}
if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) {
newErrors.handledBy = t('pickOrder.validation.handlerRequired');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = async () => {
if (!validateForm() || !formData.pickOrderId) {
return;
}

setLoading(true);
try {
await onSubmit(formData as PickExecutionIssueData);
onClose();
} catch (error) {
console.error('Error submitting pick execution issue:', error);
} finally {
setLoading(false);
}
};

const handleClose = () => {
setFormData({});
setErrors({});
onClose();
};

if (!selectedLot || !selectedPickOrderLine) {
return null;
}

const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
const requiredQty = calculateRequiredQty(selectedLot);
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* ✅ Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
<Typography variant="body2" color="warning.main">
<strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
</Typography>
</Box>
</Grid>
{/* ✅ Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
label={t('requiredQty')}
value={requiredQty || 0}
disabled
variant="outlined"
helperText={t('Still need to pick')}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label={t('remainingAvailableQty')}
value={remainingAvailableQty}
disabled
variant="outlined"
helperText={t('Available in warehouse')}
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('actualPickQty')}
type="number"
value={formData.actualPickQty || 0}
onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || t('Enter the quantity actually picked')}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('missQty')}
type="number"
value={formData.missQty || 0}
onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
error={!!errors.missQty}
helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
variant="outlined"
/>
</Grid>

<Grid item xs={12}>
<TextField
fullWidth
label={t('badItemQty')}
type="number"
value={formData.badItemQty || 0}
onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
error={!!errors.badItemQty}
helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
variant="outlined"
/>
</Grid>
{/* ✅ Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) && (
<>
<Grid item xs={12}>
<TextField
fullWidth
id="issueRemark"
label={t('issueRemark')}
multiline
rows={4}
value={formData.issueRemark || ''}
onChange={(e) => handleInputChange('issueRemark', e.target.value)}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
placeholder={t('Describe the issue with bad items')}
variant="outlined"
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth error={!!errors.handledBy}>
<InputLabel>{t('handler')}</InputLabel>
<Select
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
label={t('handler')}
>
{handlers.map((handler) => (
<MenuItem key={handler.id} value={handler.id.toString()}>
{handler.name}
</MenuItem>
))}
</Select>
{errors.handledBy && (
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
{errors.handledBy}
</Typography>
)}
</FormControl>
</Grid>
</>
)}
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={loading}
>
{loading ? t('submitting') : t('submit')}
</Button>
</DialogActions>
</Dialog>
);
};

export default PickExecutionForm;

+ 196
- 0
src/components/PickOrderSearch/PickOrderDetailsTable.tsx Wyświetl plik

@@ -0,0 +1,196 @@
"use client";

import {
Box,
Checkbox,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TablePagination,
Typography,
Paper,
} from "@mui/material";
import { useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderInfoResponse, GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
import dayjs from "dayjs";

interface PickOrderDetailsTableProps {
pickOrderDetails: GetPickOrderInfoResponse | null;
detailLoading: boolean;
selectedRowId: number | null;
onRowSelect: (lineId: number) => void;
onPageChange: (event: unknown, newPage: number) => void;
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
pageNum: number;
pageSize: number;
}

const PickOrderDetailsTable: React.FC<PickOrderDetailsTableProps> = ({
pickOrderDetails,
detailLoading,
selectedRowId,
onRowSelect,
onPageChange,
onPageSizeChange,
pageNum,
pageSize,
}) => {
const { t } = useTranslation("pickOrder");

const prepareMainTableData = useMemo(() => {
if (!pickOrderDetails) return [];
return pickOrderDetails.pickOrders.flatMap((pickOrder) =>
pickOrder.pickOrderLines.map((line) => {
const availableQty = line.availableQty ?? 0;
const balanceToPick = availableQty - line.requiredQty;
// ✅ Handle both string and array date formats from the optimized API
let formattedTargetDate = 'N/A';
if (pickOrder.targetDate) {
if (typeof pickOrder.targetDate === 'string') {
formattedTargetDate = dayjs(pickOrder.targetDate).format('YYYY-MM-DD');
} else if (Array.isArray(pickOrder.targetDate)) {
// Handle array format [2025, 9, 29, 0, 0] from optimized API
const [year, month, day] = pickOrder.targetDate;
formattedTargetDate = dayjs(`${year}-${month}-${day}`).format('YYYY-MM-DD');
}
}
return {
...line,
pickOrderCode: pickOrder.code,
targetDate: formattedTargetDate,
balanceToPick: balanceToPick,
pickedQty: line.pickedQty, // ✅ This now comes from the optimized API
availableQty: availableQty,
};
})
);
}, [pickOrderDetails]);

// ✅ Paginated data
const paginatedMainTableData = useMemo(() => {
const startIndex = pageNum * pageSize;
const endIndex = startIndex + pageSize;
return prepareMainTableData.slice(startIndex, endIndex);
}, [prepareMainTableData, pageNum, pageSize]);

if (detailLoading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<Typography variant="body2" color="text.secondary">
{t("Loading data...")}
</Typography>
</Box>
);
}

if (!pickOrderDetails) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</Box>
);
}

return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Selected")}</TableCell>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell align="right">{t("Order Quantity")}</TableCell>
<TableCell align="right">{t("Current Stock")}</TableCell>
<TableCell align="right">{t("Qty Already Picked")}</TableCell>
<TableCell align="right">{t("Stock Unit")}</TableCell>
<TableCell align="right">{t("Target Date")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paginatedMainTableData.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
</TableCell>
</TableRow>
) : (
paginatedMainTableData.map((line) => {
const availableQty = line.availableQty ?? 0;
const balanceToPick = Math.max(0, availableQty - line.requiredQty);
const actualPickedQty = line.pickedQty ?? 0;
return (
<TableRow
key={line.id}
sx={{
"& > *": { borderBottom: "unset" },
color: "black",
backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit",
cursor: "pointer",
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
<TableCell align="center" sx={{ width: "60px" }}>
<Checkbox
checked={selectedRowId === line.id}
onChange={(e) => {
if (e.target.checked) {
onRowSelect(line.id);
}
}}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell align="left">{line.pickOrderCode}</TableCell>
<TableCell align="left">{line.itemCode}</TableCell>
<TableCell align="left">{line.itemName}</TableCell>
<TableCell align="right">{line.requiredQty}</TableCell>
<TableCell align="right" sx={{
color: availableQty >= line.requiredQty ? 'success.main' : 'error.main',
}}>
{availableQty.toLocaleString()}
</TableCell>
<TableCell align="right">{actualPickedQty}</TableCell>
<TableCell align="right">{line.uomDesc}</TableCell>
<TableCell align="right">{line.targetDate}</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={prepareMainTableData.length}
page={pageNum}
rowsPerPage={pageSize}
onPageChange={onPageChange}
onRowsPerPageChange={onPageSizeChange}
rowsPerPageOptions={[10, 25, 50]}
labelRowsPerPage={t("Rows per page")}
labelDisplayedRows={({ from, to, count }) =>
`${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
}
/>
</>
);
};

export default PickOrderDetailsTable;

+ 1
- 1
src/components/PickOrderSearch/PickQcStockInModalVer3.tsx Wyświetl plik

@@ -120,7 +120,7 @@ interface LotPickData {
requiredQty: number;
actualPickQty: number;
lotStatus: string;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable';
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;


+ 1
- 1
src/components/PickOrderSearch/newcreatitem.tsx Wyświetl plik

@@ -687,7 +687,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
formProps.reset();
setHasSearched(false);
setFilteredItems([]);
alert(t("All pick orders created successfully"));
// alert(t("All pick orders created successfully"));
// 通知父组件切换到 Assign & Release 标签页
if (onPickOrderCreated) {


+ 1
- 1
src/i18n/zh/pickOrder.json Wyświetl plik

@@ -175,7 +175,7 @@
"Pick Order Details": "提料單詳情",
"Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。",
"Pick order completed successfully!": "提料單完成成功!",
"QC check failed. Lot has been rejected and marked as unavailable.": "QC 檢查失敗。批號已拒絕並標記為不可用。",
"Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。",
"This order is insufficient, please pick another lot.": "此訂單不足,請選擇其他批號。",
"Please finish QR code scan, QC check and pick order.": "請完成 QR 碼掃描、QC 檢查和提料。",
"No data available": "沒有資料",


Ładowanie…
Anuluj
Zapisz