kelvin.yau il y a 2 mois
Parent
révision
df99c77db1
8 fichiers modifiés avec 434 ajouts et 307 suppressions
  1. +1
    -0
      src/app/api/pickOrder/actions.ts
  2. +2
    -16
      src/components/DoDetail/DoInfoCard.tsx
  3. +12
    -4
      src/components/FinishedGoodSearch/FGPickOrderCard.tsx
  4. +38
    -37
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  5. +24
    -28
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  6. +320
    -220
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  7. +17
    -1
      src/i18n/zh/do.json
  8. +20
    -1
      src/i18n/zh/pickOrder.json

+ 1
- 0
src/app/api/pickOrder/actions.ts Voir le fichier

@@ -255,6 +255,7 @@ export interface FGPickOrderResponse {
shopCode: string;
shopName: string;
shopAddress: string;
ticketNo: string;
shopPoNo: string;
numberOfCartons: number;
DepartureTime: string;


+ 2
- 16
src/components/DoDetail/DoInfoCard.tsx Voir le fichier

@@ -54,14 +54,7 @@ const DoInfoCard: React.FC<Props> = ({
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
{...register("currencyCode")}
label={t("Currency Code")}
fullWidth
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
{...register("orderDate")}
@@ -78,14 +71,7 @@ const DoInfoCard: React.FC<Props> = ({
disabled={true}
/>
</Grid>
<Grid item xs={6}>
<TextField
{...register("completeDate")}
label={t("Complete Date")}
fullWidth
disabled={true}
/>
</Grid>
<Grid item xs={6}/>
</Grid>
</Box>


+ 12
- 4
src/components/FinishedGoodSearch/FGPickOrderCard.tsx Voir le fichier

@@ -20,7 +20,7 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Delivery No.")}
label={t("Delivery Code")}
fullWidth
disabled={true}
value={fgOrder.deliveryNo}
@@ -28,7 +28,7 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
</Grid>
<Grid item xs={6}>
<TextField
label={t("FG Pick Order No.")}
label={t("Pick Order Code")}
fullWidth
disabled={true}
value={fgOrder.pickOrderCode}
@@ -37,7 +37,7 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
</Grid>
<Grid item xs={6}>
<TextField
label={t("Shop PO No.")}
label={t("Shop PO Code")}
fullWidth
disabled={true}
value={fgOrder.shopPoNo}
@@ -93,6 +93,14 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
value={fgOrder.truckNo}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Ticket No.")}
fullWidth
disabled={true}
value={fgOrder.ticketNo}
/>
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
<Button
variant="contained"
@@ -100,7 +108,7 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => {
onClick={() => onQrCodeClick(fgOrder.pickOrderId)}
sx={{ minWidth: 120 }}
>
{t("View QR Code")}
{t("Print DN/Label")}
</Button>
</Grid>
</Grid>


+ 38
- 37
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Voir le fichier

@@ -57,39 +57,38 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
console.error("Missing user id in session");
return;
}
const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
console.log("Assign by store result:", res);
// Optionally show toast/refresh list here
};
// ✅ Manual assignment handler - uses the action function
const handleManualAssign = useCallback(async () => {
if (!currentUserId || isAssigning) return;
setIsAssigning(true);
try {
console.log("🎯 Manual assignment triggered for user:", currentUserId);
const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
console.log("Assign by store result:", res);
// ✅ Use the action function instead of direct fetch
const result = await autoAssignAndReleasePickOrder(currentUserId);
console.log("✅ Manual assignment result:", result);
if (result.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order manually");
// Trigger refresh of the PickExecution component
// ✅ Handle different response codes
if (res.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order to store", storeId);
// ✅ Trigger refresh to show newly assigned data
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (result.code === "EXISTS") {
console.log("ℹ️ User already has active pick orders");
// Still trigger refresh to show existing orders
} else if (res.code === "USER_BUSY") {
console.warn("⚠️ User already has pick orders in progress:", res.message);
// ✅ Show warning but still refresh to show existing orders
alert(`Warning: ${res.message}`);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "NO_ORDERS") {
console.log("ℹ️ No available pick orders for store", storeId);
alert(`Info: ${res.message}`);
} else {
console.log("ℹ️ No available pick orders or other status:", result.message);
console.log("ℹ️ Assignment result:", res.message);
alert(`Info: ${res.message}`);
}
} catch (error) {
console.error("❌ Error in manual assignment:", error);
console.error("❌ Error assigning by store:", error);
alert("Error occurred during assignment");
} finally {
setIsAssigning(false);
}
}, [currentUserId, isAssigning]);
};
// ✅ Manual assignment handler - uses the action function


const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -292,21 +291,23 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
</Typography>
</Grid>
<Grid item xs={4} display="flex" justifyContent="end" alignItems="center">
<Button
variant="contained"
onClick={() => handleAssignByStore("2/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")}
</Button>
<Button
variant="contained"
onClick={() => handleAssignByStore("4/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")}
</Button>
</Grid>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={() => handleAssignByStore("2/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")}
</Button>
<Button
variant="contained"
onClick={() => handleAssignByStore("4/F")}
disabled={isAssigning}
>
{isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")}
</Button>
</Stack>
</Grid>
</Grid>
</Stack>
</Box>
@@ -316,7 +317,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
borderBottom: '1px solid #e0e0e0'
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Execution")} iconPosition="end" />
<Tab label={t("Pick Order Detail")} iconPosition="end" />
<Tab label={t("Pick Execution Detail")} iconPosition="end" />
</Tabs>


+ 24
- 28
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx Voir le fichier

@@ -87,10 +87,12 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
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);
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// ✅ Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
@@ -166,36 +168,30 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
// ✅ 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');
newErrors.actualPickQty = t('Qty is required');
}
// ✅ FIXED: Check if actual pick qty exceeds remaining available qty
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
}
// ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty)
if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
}
// ✅ 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');
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
}
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;
};
@@ -251,7 +247,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
<TextField
fullWidth
label={t('Required Qty')}
value={requiredQty || 0}
value={selectedLot?.requiredQty || 0}
disabled
variant="outlined"
// helperText={t('Still need to pick')}
@@ -277,7 +273,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
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')}
helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
variant="outlined"
/>
</Grid>


+ 320
- 220
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Voir le fichier

@@ -15,6 +15,7 @@ import {
TableHead,
TableRow,
Paper,
Checkbox,
TablePagination,
Modal,
} from "@mui/material";
@@ -349,6 +350,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
// ✅ Add these missing state variables after line 352
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
const fetchFgPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
@@ -393,13 +399,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
// TODO: Implement QR code functionality
};

useEffect(() => {
startScan();
return () => {
stopScan();
resetScan();
};
}, [startScan, stopScan, resetScan]);

const fetchAllCombinedLotData = useCallback(async (userId?: number) => {
setCombinedDataLoading(true);
@@ -454,14 +453,12 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
};
}, [fetchAllCombinedLotData]);

// ✅ Handle QR code submission for matched lot (external scanning)
// ✅ Handle QR code submission for matched lot (external scanning)
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(`✅ Processing QR Code for lot: ${lotNo}`);
// ✅ Use current data without refreshing to avoid infinite loop
const currentLotData = combinedLotData;
console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
const matchingLots = currentLotData.filter(lot =>
lot.lotNo === lotNo ||
@@ -480,46 +477,56 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
try {
let successCount = 0;
let existsCount = 0;
let errorCount = 0;
for (const matchingLot of matchingLots) {
console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
if (matchingLot.stockOutLineId) {
console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
existsCount++;
// ✅ FIXED: Only update status to 'checked', keep qty at 0
const stockOutLineUpdate = await updateStockOutLineStatus({
id: matchingLot.stockOutLineId,
status: 'checked',
qty: 0 // ✅ Keep qty at 0 until user actually submits
});
console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
if (stockOutLineUpdate && stockOutLineUpdate.code === "SUCCESS") {
console.log(`✅ Stock out line updated successfully for line ${matchingLot.pickOrderLineId}`);
successCount++;
} else {
console.error(`❌ Failed to update stock out line for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
errorCount++;
}
} else {
const stockOutLineData: CreateStockOutLine = {
// ✅ If no stock out line exists, create one with qty = 0
const createStockOutLineData = {
consoCode: matchingLot.pickOrderConsoCode,
pickOrderLineId: matchingLot.pickOrderLineId,
inventoryLotLineId: matchingLot.lotId,
qty: 0.0
qty: 0 // ✅ Create with qty = 0
};
console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
const result = await createStockOutLine(stockOutLineData);
console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
const createResult = await createStockOutLine(createStockOutLineData);
console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
if (result && result.code === "EXISTS") {
console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
existsCount++;
} else if (result && result.code === "SUCCESS") {
if (createResult && createResult.code === "SUCCESS") {
console.log(`✅ Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
successCount++;
} else {
console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, createResult);
errorCount++;
}
}
}
// ✅ Always refresh data after processing (success or failure)
// ✅ FIXED: Set refresh flag before refreshing data
setIsRefreshingData(true);
console.log("🔄 Refreshing data after QR code processing...");
await fetchAllCombinedLotData();
if (successCount > 0 || existsCount > 0) {
console.log(`✅ QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
if (successCount > 0) {
console.log(`✅ QR Code processing completed: ${successCount} updated/created`);
setQrScanSuccess(true);
setQrScanInput(''); // Clear input after successful processing
@@ -543,12 +550,18 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(false);
// ✅ Still refresh data even on error
setIsRefreshingData(true);
await fetchAllCombinedLotData();
// ✅ Clear error state after a delay
setTimeout(() => {
setQrScanError(false);
}, 3000);
} finally {
// ✅ Clear refresh flag after a short delay
setTimeout(() => {
setIsRefreshingData(false);
}, 1000);
}
}, [combinedLotData, fetchAllCombinedLotData]);

@@ -567,16 +580,15 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const lotId = selectedLotForQr.lotId;
// Create stock out line
const stockOutLineData: CreateStockOutLine = {
consoCode: selectedLotForQr.pickOrderConsoCode, // ✅ Use pickOrderConsoCode instead of pickOrderCode
pickOrderLineId: selectedLotForQr.pickOrderLineId,
inventoryLotLineId: selectedLotForQr.lotId,
qty: 0.0
};

try {
await createStockOutLine(stockOutLineData);
console.log("Stock out line created successfully!");
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: selectedLotForQr.stockOutLineQty || 0
});
console.log("Stock out line updated successfully!");
// Close modal
setQrModalOpen(false);
@@ -602,15 +614,28 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {

// ✅ Outside QR scanning - process QR codes from outside the page automatically
useEffect(() => {
if (qrValues.length > 0 && combinedLotData.length > 0) {
const latestQr = qrValues[qrValues.length - 1];
// ✅ Don't process QR codes when refreshing data or if not manually scanning
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
return;
}
const latestQr = qrValues[qrValues.length - 1];
// ✅ Prevent processing the same QR code multiple times
if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
console.log(" QR code already processed, skipping...");
return;
}
if (latestQr && latestQr !== lastProcessedQr) {
console.log(` Processing new QR code: ${latestQr}`);
setLastProcessedQr(latestQr);
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
// Extract lot number from QR code
let lotNo = '';
try {
const qrData = JSON.parse(latestQr);
if (qrData.stockInLineId && qrData.itemId) {
// For JSON QR codes, we need to fetch the lot number
fetchStockInLineInfo(qrData.stockInLineId)
.then((stockInLineInfo) => {
console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
@@ -623,20 +648,18 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
.catch((error) => {
console.error("Outside QR scan - Error fetching stock in line info:", error);
});
return; // Exit early for JSON QR codes
return;
}
} catch (error) {
// Not JSON format, treat as direct lot number
lotNo = latestQr.replace(/[{}]/g, '');
}
// For direct lot number QR codes
if (lotNo) {
console.log(`Outside QR scan detected (direct): ${lotNo}`);
handleQrCodeSubmit(lotNo);
}
}
}, [qrValues, combinedLotData, handleQrCodeSubmit]);
}, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, handleQrCodeSubmit]);


const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
@@ -695,13 +718,19 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
try {
// ✅ FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + newQty;
// ✅ FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
newStatus = 'completed';
} else if (cumulativeQty > 0) {
newStatus = 'partially_completed';
} else {
newStatus = 'checked'; // QR scanned but no quantity submitted yet
}
console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
@@ -716,7 +745,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty
qty: cumulativeQty // ✅ Use cumulative quantity
});
if (newQty > 0) {
@@ -728,12 +757,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// ✅ FIXED: Use the proper API function instead of direct fetch
// ✅ Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
// ✅ Use the imported API function instead of direct fetch
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(`✅ Pick order completion check result:`, completionResponse);
@@ -907,64 +935,128 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}, []);

// Pagination data with sorting by routerIndex
const paginatedData = useMemo(() => {
// ✅ Sort by routerIndex first, then by other criteria
const sortedData = [...combinedLotData].sort((a, b) => {
const aIndex = a.routerIndex || 0;
const bIndex = b.routerIndex || 0;
// Primary sort: by routerIndex
if (aIndex !== bIndex) {
return aIndex - bIndex;
}
// Remove the sorting logic and just do pagination
const paginatedData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return combinedLotData.slice(startIndex, endIndex); // ✅ No sorting needed
}, [combinedLotData, paginationController]);
const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
return;
}
try {
// ✅ FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// ✅ FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
newStatus = 'completed';
} else if (cumulativeQty > 0) {
newStatus = 'partially_completed';
} else {
newStatus = 'checked'; // QR scanned but no quantity submitted yet
}
console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Required Qty: ${lot.requiredQty}`);
console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
console.log(`New Submitted Qty: ${submitQty}`);
console.log(`Cumulative Qty: ${cumulativeQty}`);
console.log(`New Status: ${newStatus}`);
console.log(`=====================================`);
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // ✅ Use cumulative quantity
});
if (submitQty > 0) {
await updateInventoryLotLineQuantities({
inventoryLotLineId: lot.lotId,
qty: submitQty,
status: 'available',
operation: 'pick'
});
}
// ✅ Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
// Secondary sort: by pickOrderCode if routerIndex is the same
if (a.pickOrderCode !== b.pickOrderCode) {
return a.pickOrderCode.localeCompare(b.pickOrderCode);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(`✅ Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
} 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);
}
// Tertiary sort: by lotNo if everything else is the same
return (a.lotNo || '').localeCompare(b.lotNo || '');
});
}
await fetchAllCombinedLotData();
console.log("Pick quantity submitted successfully!");
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return sortedData.slice(startIndex, endIndex);
}, [combinedLotData, paginationController]);
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
} catch (error) {
console.error("Error submitting pick quantity:", error);
}
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);


// ✅ Add these functions after line 395
const handleStartScan = useCallback(() => {
console.log(" Starting manual QR scan...");
setIsManualScanning(true);
setProcessedQrCodes(new Set());
setLastProcessedQr('');
startScan();
}, [startScan]);

const handleStopScan = useCallback(() => {
console.log("⏹️ Stopping manual QR scan...");
setIsManualScanning(false);
stopScan();
resetScan();
}, [stopScan, resetScan]);
const getStatusMessage = useCallback((lot: any) => {
switch (lot.stockOutLineStatus?.toLowerCase()) {
case 'pending':
return t("Please finish QR code scan and 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("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 and pick order.");
}
}, [t]);
return (
<FormProvider {...formProps}>
{/* Search Box */}
{/*
<Box>

{fgPickOrdersLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{fgPickOrders.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t("No FG pick orders found")}
</Typography>
</Box>
) : (
fgPickOrders.map((fgOrder) => (
<FGPickOrderCard
key={fgOrder.pickOrderId}
fgOrder={fgOrder}
onQrCodeClick={handleQrCodeClick}
/>
))
)}
</Box>
)}
</Box>
*/}


<Stack spacing={2}>
{/* DO Header */}
@@ -980,11 +1072,15 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
<strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Delivery Date")}:</strong> {(fgPickOrders[0].deliveryDate || '-').split('T')[0]}
<strong>{t("Pick Order Code")}:</strong>{fgPickOrders[0].pickOrderCode || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
</Typography>

</Stack>
</Paper>
)
@@ -997,8 +1093,39 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
<Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
{t("All Pick Order Lots")}
</Typography>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
{!isManualScanning ? (
<Button
variant="contained"
startIcon={<QrCodeIcon />}
onClick={handleStartScan}
color="primary"
sx={{ minWidth: '120px' }}
>
{t("Start QR Scan")}
</Button>
) : (
<Button
variant="outlined"
startIcon={<QrCodeIcon />}
onClick={handleStopScan}
color="secondary"
sx={{ minWidth: '120px' }}
>
{t("Stop QR Scan")}
</Button>
)}
{isManualScanning && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="caption" color="primary">
{t("Scanning...")}
</Typography>
</Box>
)}
</Box>
</Box>
<TableContainer component={Paper}>
@@ -1013,9 +1140,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
{/* <TableCell>{t("Lot Location")}</TableCell> */}
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
{/* <TableCell align="right">{t("Original Available Qty")}</TableCell> */}
<TableCell align="center">{t("Lot Actual Pick Qty")}</TableCell>
<TableCell align="right">{t("Scan Result")}</TableCell>
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
{/* <TableCell align="right">{t("Remaining Available Qty")}</TableCell> */}
<TableCell align="center">{t("Action")}</TableCell>
{/* <TableCell align="center">{t("Action")}</TableCell> */}
</TableRow>
</TableHead>
<TableBody>
@@ -1041,7 +1170,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
>
<TableCell>
<Typography variant="body2" fontWeight="bold">
{lot.routerIndex || index + 1}
{index + 1}
</Typography>
</TableCell>
<TableCell>
@@ -1049,7 +1178,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
{lot.routerRoute || '-'}
</Typography>
</TableCell>
<TableCell>{lot.itemName}</TableCell>
<TableCell>{lot.itemName+'('+lot.stockUnit+')'}</TableCell>
<TableCell>
<Box>
<Typography
@@ -1068,132 +1197,103 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
<TableCell align="right">
{(() => {
const inQty = lot.inQty || 0;
const requiredQty = lot.requiredQty || 0;
const actualPickQty = lot.actualPickQty || 0;
const outQty = lot.outQty || 0;
const result = inQty - outQty;
return result.toLocaleString();
const result = requiredQty;
return result.toLocaleString()+'('+lot.uomShortDesc+')';
})()}
</TableCell>
<TableCell align="center">
{/* ✅ QR Scan Button if not scanned, otherwise show TextField + Issue button */}
{lot.stockOutLineStatus?.toLowerCase() === 'pending' ? (
<Button
variant="outlined"
size="small"
onClick={() => {
setSelectedLotForQr(lot);
setQrModalOpen(true);
resetScan();
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected')
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '40px',
whiteSpace: 'nowrap',
minWidth: '80px',
}}
startIcon={<QrCodeIcon />}
title="Click to scan QR code"
>
{t("Scan")}
</Button>
) : (
// ✅ When stockOutLineId exists, show TextField + Issue button
<Stack direction="row" spacing={1} alignItems="center">
<TextField
type="number"
size="small"
value={pickQtyData[`${lot.pickOrderLineId}-${lot.lotId}`] || ''}
onChange={(e) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
handlePickQtyChange(lotKey, parseFloat(e.target.value) || 0);
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed'
}
inputProps={{
min: 0,
max: calculateRemainingRequiredQty(lot),
step: 0.01
}}
sx={{
width: '60px',
height: '28px',
'& .MuiInputBase-input': {
fontSize: '0.7rem',
textAlign: 'center',
padding: '6px 8px'
}
}}
placeholder="0"
/>
{lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
<Checkbox
checked={lot.stockOutLineStatus?.toLowerCase() !== 'pending'}
disabled={true}
readOnly={true}
sx={{
color: lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400',
'&.Mui-checked': {
color: 'success.main',
},
}}
/>
) : null}
</TableCell>
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Stack direction="row" spacing={1} alignItems="center">
<Button
variant="contained"
onClick={() => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
// Submit with default lot required pick qty
<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">
{(() => {
const inQty = lot.inQty || 0;
const outQty = lot.outQty || 0;
const result = inQty - outQty;
return result.toLocaleString();
})()}
</TableCell> */}
<TableCell align="center">
<Stack direction="column" spacing={1} alignItems="center">
<Button
variant="contained"
onClick={() => {
handleSubmitPickQty(lot);
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
!pickQtyData[`${lot.pickOrderLineId}-${lot.lotId}`] ||
!lot.stockOutLineStatus ||
!['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase())
}
sx={{
fontSize: '0.75rem',
py: 0.5,
minHeight: '28px'
}}
>
{t("Submit")}
</Button>
</Stack>
</TableCell>
</TableRow>
handlePickQtyChange(lotKey, submitQty);
handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed
}
sx={{
fontSize: '0.75rem',
py: 0.5,
minHeight: '28px',
minWidth: '70px'
}}
>
{t("Submit")}
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' || // ✅ Disable when finished
lot.stockOutLineStatus === 'pending' // ✅ Disable when QR scan not passed
}
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>
</Box>
</TableCell>

</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{/* ✅ Status Messages Display - Move here, outside the table */}
{paginatedData.length > 0 && (
<Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
{paginatedData.map((lot, index) => (
<Box key={`${lot.pickOrderLineId}-${lot.lotId}`} sx={{ mb: 1 }}>
<Typography variant="body2" color="text.secondary">
<strong>{t("Lot")} {lot.lotNo}:</strong> {getStatusMessage(lot)}
</Typography>
</Box>
))}
</Box>
)}
<TablePagination
component="div"
count={combinedLotData.length}


+ 17
- 1
src/i18n/zh/do.json Voir le fichier

@@ -24,6 +24,22 @@
"Selected Item(s): ": "總貨品數量: ",
"Confirm": "確認",
"Cancel": "取消",
"Releasing": "處理中"
"Releasing": "處理中",
"Shop Code": "店鋪編號",
"Supplier Code": "供應商編號",
"Estimated Arrival Date": "預計到貨日期",
"Item No.": "商品編號",
"Item Name": "商品名稱",
"Quantity": "數量",
"uom": "單位",
"Lot No.": "批號",
"Expiry Date": "有效期",
"Location": "庫位",
"Price": "價格",
"Action": "操作",
"Edit": "編輯",
"Delete": "刪除",
"Release": "放單",
"Back": "返回"

}

+ 20
- 1
src/i18n/zh/pickOrder.json Voir le fichier

@@ -242,5 +242,24 @@
"Max":"最大值",
"Route":"路線",
"Index":"編號",
"No FG pick orders found":"沒有成品提料單"
"No FG pick orders found":"沒有成品提料單",
"Finish Scan?":"完成掃描?",
"Delivery Code":"出倉單編號",
"Shop PO Code":"訂單編號",
"Shop ID":"商店編號",
"Truck No.":"車輛編號",
"Departure Time":"車輛出發時間",
"Shop Name":"商店名稱",
"Shop Address":"商店地址",
"Delivery Date":"目標日期",
"Pick Execution 2/F":"進行提料 2/F",
"Pick Execution 4/F":"進行提料 4/F",
"Pick Execution Detail":"進行提料詳情",
"Submit Required Pick Qty":"提交所需提料數量",
"Scan Result":"掃描結果",
"Ticket No.":"提票號碼",
"Start QR Scan":"開始QR掃描",
"Stop QR Scan":"停止QR掃描",
"Scanning...":"掃描中...",
"Print DN/Label":"列印送貨單/標籤"
}

Chargement…
Annuler
Enregistrer