Browse Source

update

master
CANCERYS\kw093 1 month ago
parent
commit
0b83c25e55
7 changed files with 495 additions and 385 deletions
  1. +8
    -6
      src/app/api/pickOrder/actions.ts
  2. +27
    -18
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  3. +315
    -338
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  4. +76
    -17
      src/components/ProductionProcess/MachineScanner.tsx
  5. +41
    -6
      src/components/ProductionProcess/OperatorScanner.tsx
  6. +17
    -0
      src/components/ProductionProcess/ProductionRecordingModal.tsx
  7. +11
    -0
      src/i18n/zh/pickOrder.json

+ 8
- 6
src/app/api/pickOrder/actions.ts View File

@@ -247,12 +247,13 @@ export interface UpdateSuggestedLotLineIdRequest {
}
export interface FGPickOrderResponse {
// ✅ 新增:支持多个 pick orders
doPickOrderId: number; // ✅ 新增:do_pick_order 的 ID
pickOrderIds?: number[]; // ✅ 新增:所有 pick order IDs
pickOrderCodes?: string; // ✅ 新增:所有 pick order codes(逗号分隔)
deliveryOrderIds?: number[]; // ✅ 新增:所有 delivery order IDs
deliveryNos?: string; // ✅ 新增:所有 delivery order codes(逗号分隔)
numberOfPickOrders?: number; // ✅ 新增:pick order 数量
doPickOrderId: number;
pickOrderIds?: number[];
pickOrderCodes?: string[]; // ✅ 改为数组
deliveryOrderIds?: number[];
deliveryNos?: string[]; // ✅ 改为数组
numberOfPickOrders?: number;
lineCountsPerPickOrder?: number[];// ✅ 新增:pick order 数量
// ✅ 保留原有字段用于向后兼容(显示第一个 pick order)
pickOrderId: number;
@@ -274,6 +275,7 @@ export interface FGPickOrderResponse {
truckLanceCode: string;
storeId: string;
qrCodeData: number;

}
export interface DoPickOrderDetail {
doPickOrder: {


+ 27
- 18
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx View File

@@ -426,6 +426,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
itemName: line.item.name,
lotNo: lot.lotNo,
location: lot.location,
deliveryOrderCode: po.deliveryOrderCode,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
processingStatus: lot.processingStatus,
@@ -549,23 +550,30 @@ if (showDetailView && selectedDoPickOrder) {
<Stack spacing={2}>
{/* ✅ 按 pickOrderCode 分组 */}
{Object.entries(
detailLotData.reduce((acc: any, lot: any) => {
const key = lot.pickOrderCode || 'Unknown';
if (!acc[key]) acc[key] = [];
acc[key].push(lot);
return acc;
}, {})
).map(([pickOrderCode, lots]: [string, any]) => (
<Accordion key={pickOrderCode} defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">
{t("Pick Order")}: {pickOrderCode} ({(lots as any[]).length} {t("items")})
</Typography>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
detailLotData.reduce((acc: any, lot: any) => {
const key = lot.pickOrderCode || 'Unknown';
if (!acc[key]) {
acc[key] = {
lots: [],
deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // ✅ 保存对应的 deliveryOrderCode
};
}
acc[key].lots.push(lot);
return acc;
}, {})
).map(([pickOrderCode, data]: [string, any]) => (
<Accordion key={pickOrderCode} defaultExpanded={true}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1" fontWeight="bold">
{t("Pick Order")}: {pickOrderCode} ({data.lots.length} {t("items")})
{" | "}
{t("Delivery Order")}: {data.deliveryOrderCode} {/* ✅ 使用保存的 deliveryOrderCode */}
</Typography>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>{t("Index")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
@@ -578,7 +586,7 @@ if (showDetailView && selectedDoPickOrder) {
</TableRow>
</TableHead>
<TableBody>
{(lots as any[]).map((lot: any, index: number) => (
{data.lots.map((lot: any, index: number) => (
<TableRow key={index}>
<TableCell>{index + 1}</TableCell>
<TableCell>{lot.itemCode || 'N/A'}</TableCell>
@@ -651,6 +659,7 @@ if (showDetailView && selectedDoPickOrder) {
<Typography variant="h6">
{doPickOrder.pickOrderCode}
</Typography>

<Typography variant="body2" color="text.secondary">
{doPickOrder.shopName} - {doPickOrder.deliveryNo}
</Typography>


+ 315
- 338
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -80,9 +80,7 @@ const QrCodeModal: React.FC<{
const { t } = useTranslation("pickOrder");
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [manualInput, setManualInput] = useState<string>('');
const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
const [pickOrderSwitching, setPickOrderSwitching] = useState(false);

const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
const [manualInputError, setManualInputError] = useState<boolean>(false);
const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
@@ -378,33 +376,8 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
const fetchFgPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
setFgPickOrdersLoading(true);
try {
const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
console.log("🔍 DEBUG: Fetched FG pick orders:", fgPickOrders);
console.log("🔍 DEBUG: First order numberOfPickOrders:", fgPickOrders[0]?.numberOfPickOrders);
setFgPickOrders(fgPickOrders);
// ✅ 移除:不需要再单独调用 fetchDoPickOrderDetail
// all-lots-hierarchical API 已经包含了所有需要的数据
} catch (error) {
console.error("❌ Error fetching FG pick orders:", error);
setFgPickOrders([]);
} finally {
setFgPickOrdersLoading(false);
}
}, [currentUserId]);
useEffect(() => {
if (combinedLotData.length > 0) {
fetchFgPickOrdersData();
}
}, [combinedLotData, fetchFgPickOrdersData]);
// ✅ Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => {
@@ -443,187 +416,102 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setAllLotsCompleted(allCompleted);
return allCompleted;
}, []);
const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {

setCombinedDataLoading(true);
try {
const userIdToUse = userId || currentUserId;
console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse);
// 在 fetchAllCombinedLotData 函数中(约 446-684 行)

if (!userIdToUse) {
console.warn("⚠️ No userId available, skipping API call");
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
return;
}
// ✅ 获取新结构的层级数据
// ✅ 获取新结构的层级数据
const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
console.log("✅ Hierarchical data (new structure):", hierarchicalData);
// ✅ 检查数据结构
if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders) {
console.warn("⚠️ No FG info or pick orders found");
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
return;
}
// ✅ 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
const fgOrder: FGPickOrderResponse = {
doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
ticketNo: hierarchicalData.fgInfo.ticketNo,
storeId: hierarchicalData.fgInfo.storeId,
shopCode: hierarchicalData.fgInfo.shopCode,
shopName: hierarchicalData.fgInfo.shopName,
truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
DepartureTime: hierarchicalData.fgInfo.departureTime,
shopAddress: "",
// ✅ 从第一个 pick order 获取兼容字段
pickOrderId: hierarchicalData.pickOrders[0]?.pickOrderId || 0,
pickOrderCode: hierarchicalData.pickOrders[0]?.pickOrderCode || "",
pickOrderConsoCode: hierarchicalData.pickOrders[0]?.consoCode || "",
pickOrderTargetDate: hierarchicalData.pickOrders[0]?.targetDate || "",
pickOrderStatus: hierarchicalData.pickOrders[0]?.status || "",
deliveryOrderId: hierarchicalData.pickOrders[0]?.doOrderId || 0,
deliveryNo: hierarchicalData.pickOrders[0]?.deliveryOrderCode || "",
deliveryDate: "",
shopId: 0,
shopPoNo: "",
numberOfCartons: 0,
qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
// ✅ 新增:多个 pick orders 信息
numberOfPickOrders: hierarchicalData.pickOrders.length,
pickOrderIds: hierarchicalData.pickOrders.map((po: any) => po.pickOrderId),
pickOrderCodes: hierarchicalData.pickOrders.map((po: any) => po.pickOrderCode).join(", "),
deliveryOrderIds: hierarchicalData.pickOrders.map((po: any) => po.doOrderId),
deliveryNos: hierarchicalData.pickOrders.map((po: any) => po.deliveryOrderCode).join(", ")
};
setFgPickOrders([fgOrder]);
// ✅ 构建 doPickOrderDetail(用于 switcher)
if (hierarchicalData.pickOrders.length > 1) {
const detail: DoPickOrderDetail = {
doPickOrder: {
id: hierarchicalData.fgInfo.doPickOrderId,
store_id: hierarchicalData.fgInfo.storeId,
ticket_no: hierarchicalData.fgInfo.ticketNo,
ticket_status: "",
truck_id: 0,
truck_departure_time: hierarchicalData.fgInfo.departureTime,
shop_id: 0,
handled_by: null,
loading_sequence: 0,
ticket_release_time: null,
TruckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
ShopCode: hierarchicalData.fgInfo.shopCode,
ShopName: hierarchicalData.fgInfo.shopName,
RequiredDeliveryDate: ""
},
pickOrders: hierarchicalData.pickOrders.map((po: any) => ({
pick_order_id: po.pickOrderId,
pick_order_code: po.pickOrderCode,
do_order_id: po.doOrderId,
delivery_order_code: po.deliveryOrderCode,
consoCode: po.consoCode,
status: po.status,
targetDate: po.targetDate
})),
selectedPickOrderId: pickOrderIdOverride || hierarchicalData.pickOrders[0]?.pickOrderId || 0,
lotDetails: []
};
setDoPickOrderDetail(detail);
// ✅ 设置默认选中的 pick order ID
if (!selectedPickOrderId) {
setSelectedPickOrderId(pickOrderIdOverride || hierarchicalData.pickOrders[0]?.pickOrderId);
}
}
// ✅ 确定要显示的 pick order
const targetPickOrderId = pickOrderIdOverride || selectedPickOrderId || hierarchicalData.pickOrders[0]?.pickOrderId;
// ✅ 找到对应的 pick order 数据
const targetPickOrder = hierarchicalData.pickOrders.find((po: any) =>
po.pickOrderId === targetPickOrderId
);
if (!targetPickOrder) {
console.warn("⚠️ Target pick order not found:", targetPickOrderId);
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
return;
}
console.log("🎯 Displaying pick order:", targetPickOrder.pickOrderCode);
// ✅ 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = [];
targetPickOrder.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
// ✅ 有 lots 的情况
line.lots.forEach((lot: any) => {
flatLotData.push({
pickOrderId: targetPickOrder.pickOrderId,
pickOrderCode: targetPickOrder.pickOrderCode,
pickOrderConsoCode: targetPickOrder.consoCode,
pickOrderTargetDate: targetPickOrder.targetDate,
pickOrderStatus: targetPickOrder.status,
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
//uomCode: line.item.uomCode,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
lotId: lot.id,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate,
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
inQty: lot.inQty,
outQty: lot.outQty,
holdQty: lot.holdQty,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability,
processingStatus: lot.processingStatus,
suggestedPickLotId: lot.suggestedPickLotId,
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
routerId: lot.router?.id,
routerIndex: lot.router?.index,
routerRoute: lot.router?.route,
routerArea: lot.router?.area,
});
});
} else {
// ✅ 没有 lots 的情况(null stock)- 也要显示
const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
setCombinedDataLoading(true);
try {
const userIdToUse = userId || currentUserId;
console.log("🔍 fetchAllCombinedLotData called with userId:", userIdToUse);
if (!userIdToUse) {
console.warn("⚠️ No userId available, skipping API call");
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
return;
}
// ✅ 获取新结构的层级数据
const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
console.log("✅ Hierarchical data (new structure):", hierarchicalData);
// ✅ 检查数据结构
if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) {
console.warn("⚠️ No FG info or pick orders found");
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
return;
}
// ✅ 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
const mergedPickOrder = hierarchicalData.pickOrders[0];
// ✅ 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
// 修改第 478-509 行的 fgOrder 构建逻辑:

const fgOrder: FGPickOrderResponse = {
doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
ticketNo: hierarchicalData.fgInfo.ticketNo,
storeId: hierarchicalData.fgInfo.storeId,
shopCode: hierarchicalData.fgInfo.shopCode,
shopName: hierarchicalData.fgInfo.shopName,
truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
DepartureTime: hierarchicalData.fgInfo.departureTime,
shopAddress: "",
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
// ✅ 兼容字段
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
pickOrderConsoCode: mergedPickOrder.consoCode || "",
pickOrderTargetDate: mergedPickOrder.targetDate || "",
pickOrderStatus: mergedPickOrder.status || "",
deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0,
deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "",
deliveryDate: "",
shopId: 0,
shopPoNo: "",
numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
// ✅ 新增:多个 pick orders 信息 - 保持数组格式,不要 join
numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
pickOrderIds: mergedPickOrder.pickOrderIds || [],
pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
? mergedPickOrder.pickOrderCodes
: [], // ✅ 改:保持数组
deliveryOrderIds: mergedPickOrder.doOrderIds || [],
deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
? mergedPickOrder.deliveryOrderCodes
: [], // ✅ 改:保持数组
lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder)
? mergedPickOrder.lineCountsPerPickOrder
: []
};
setFgPickOrders([fgOrder]);
console.log("🔍 DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder);
console.log("🔍 DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
// ❌ 移除:不需要 doPickOrderDetail 和 switcher 逻辑
// if (hierarchicalData.pickOrders.length > 1) { ... }
// ✅ 直接使用合并后的 pickOrderLines
console.log("🎯 Displaying merged pick order lines");
// ✅ 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = [];
mergedPickOrder.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
// ✅ 有 lots 的情况
line.lots.forEach((lot: any) => {
flatLotData.push({
pickOrderId: targetPickOrder.pickOrderId,
pickOrderCode: targetPickOrder.pickOrderCode,
pickOrderConsoCode: targetPickOrder.consoCode,
pickOrderTargetDate: targetPickOrder.targetDate,
pickOrderStatus: targetPickOrder.status,
// ✅ 使用合并后的数据
pickOrderConsoCode: mergedPickOrder.consoCode,
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
@@ -632,56 +520,95 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
//uomCode: line.item.uomCode,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
// ✅ Null stock 字段
lotId: null,
lotNo: null,
expiryDate: null,
location: null,
stockUnit: line.item.uomDesc,
availableQty: 0,
requiredQty: line.requiredQty,
actualPickQty: 0,
inQty: 0,
outQty: 0,
holdQty: 0,
lotStatus: 'unavailable',
lotAvailability: 'insufficient_stock',
processingStatus: 'pending',
suggestedPickLotId: null,
stockOutLineId: null,
stockOutLineStatus: null,
stockOutLineQty: 0,
lotId: lot.id,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate,
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
inQty: lot.inQty,
outQty: lot.outQty,
holdQty: lot.holdQty,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability,
processingStatus: lot.processingStatus,
suggestedPickLotId: lot.suggestedPickLotId,
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
routerId: null,
routerIndex: 999999, // ✅ 放到最后
routerRoute: null,
routerArea: null,
uomShortDesc: line.item.uomShortDesc
routerId: lot.router?.id,
routerIndex: lot.router?.index,
routerRoute: lot.router?.route,
routerArea: lot.router?.area,
});
}
});
});
} else {
// ✅ 没有 lots 的情况(null stock)
flatLotData.push({
pickOrderConsoCode: mergedPickOrder.consoCode,
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
// ✅ Null stock 字段
lotId: null,
lotNo: null,
expiryDate: null,
location: null,
stockUnit: line.item.uomDesc,
availableQty: 0,
requiredQty: line.requiredQty,
actualPickQty: 0,
inQty: 0,
outQty: 0,
holdQty: 0,
lotStatus: 'unavailable',
lotAvailability: 'insufficient_stock',
processingStatus: 'pending',
suggestedPickLotId: null,
stockOutLineId: null,
stockOutLineStatus: null,
stockOutLineQty: 0,
routerId: null,
routerIndex: 999999,
routerRoute: null,
routerArea: null,
});
}
});

console.log("✅ Transformed flat lot data:", flatLotData);
console.log("🔍 Total items (including null stock):", flatLotData.length);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
checkAllLotsCompleted(flatLotData);
} catch (error) {
console.error("❌ Error fetching combined lot data:", error);
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
} finally {
setCombinedDataLoading(false);
}
}, [currentUserId, selectedPickOrderId, checkAllLotsCompleted]);
console.log("✅ Transformed flat lot data:", flatLotData);
console.log("🔍 Total items (including null stock):", flatLotData.length);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
checkAllLotsCompleted(flatLotData);
} catch (error) {
console.error("❌ Error fetching combined lot data:", error);
setCombinedLotData([]);
setOriginalCombinedData([]);
setAllLotsCompleted(false);
} finally {
setCombinedDataLoading(false);
}
}, [currentUserId, checkAllLotsCompleted]); // ❌ 移除 selectedPickOrderId 依赖
// ✅ Add effect to check completion when lot data changes
useEffect(() => {
if (combinedLotData.length > 0) {
@@ -1687,94 +1614,144 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
</Paper>
)
)}
{/* ✅ 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
<Box>
{/* ✅ FG Info Card */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<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>
)}
{/* ✅ 保留:Submit All Scanned Button */}
<Button
variant="contained"
color="success"
onClick={handleSubmitAllScanned}
disabled={scannedItemsCount === 0 || isSubmittingAll}
sx={{ minWidth: '160px' }}
>
{isSubmittingAll ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{t("Submitting...")}
</>
) : (
`${t("Submit All Scanned")} (${scannedItemsCount})`
)}
</Button>
</Box>
</Box>

{fgPickOrders.length > 0 && (
<Paper sx={{ p: 2, mb: 2 }}>
<Stack spacing={2}>
{/* 基本信息 */}
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
</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>
{/* ✅ 改进:三个字段显示在一起,使用表格式布局 */}
{/* ✅ 改进:三个字段合并显示 */}
{/* ✅ 改进:表格式显示每个 pick order */}
<Box sx={{
p: 2,
backgroundColor: '#f5f5f5',
borderRadius: 1
}}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("Pick Orders Details")}:
</Typography>
{(() => {
const pickOrderCodes = fgPickOrders[0].pickOrderCodes as string[] | string | undefined;
const deliveryNos = fgPickOrders[0].deliveryNos as string[] | string | undefined;
const lineCounts = fgPickOrders[0].lineCountsPerPickOrder;
const pickOrderCodesArray = Array.isArray(pickOrderCodes)
? pickOrderCodes
: (typeof pickOrderCodes === 'string' ? pickOrderCodes.split(', ') : []);
const deliveryNosArray = Array.isArray(deliveryNos)
? deliveryNos
: (typeof deliveryNos === 'string' ? deliveryNos.split(', ') : []);
{/* ✅ Pick Order Switcher - 放在 FG Info 下面,QR 按钮上面 */}
{doPickOrderDetail && doPickOrderDetail.pickOrders.length > 1 && (
<Box sx={{ mb: 2, mt: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("Select Pick Order:")}
const lineCountsArray = Array.isArray(lineCounts) ? lineCounts : [];
const maxLength = Math.max(
pickOrderCodesArray.length,
deliveryNosArray.length,
lineCountsArray.length
);
if (maxLength === 0) {
return <Typography variant="body2" color="text.secondary">-</Typography>;
}
// ✅ 使用与外部基本信息相同的样式
return Array.from({ length: maxLength }, (_, idx) => (
<Stack
key={idx}
direction="row"
spacing={4}
useFlexGap
flexWrap="wrap"
sx={{ mb: idx < maxLength - 1 ? 1 : 0 }} // 除了最后一行,都添加底部间距
>
<Typography variant="subtitle1">
<strong>{t("Delivery Order")}:</strong> {deliveryNosArray[idx] || '-'}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{doPickOrderDetail.pickOrders.map((po: any) => (
<Chip
key={po.pick_order_id}
label={`${po.pick_order_code} (${po.delivery_order_code})`}
onClick={() => handlePickOrderSwitch(po.pick_order_id)}
color={selectedPickOrderId === po.pick_order_id ? "primary" : "default"}
variant={selectedPickOrderId === po.pick_order_id ? "filled" : "outlined"}
sx={{
cursor: 'pointer',
'&:hover': { backgroundColor: 'primary.light', color: 'white' }
}}
/>
))}
</Box>
</Box>
<Typography variant="subtitle1">
<strong>{t("Pick Order")}:</strong> {pickOrderCodesArray[idx] || '-'}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Finsihed good items")}:</strong> {lineCountsArray[idx] || '-'}<strong>{t("kinds")}</strong>
</Typography>
</Stack>
));
})()}
</Box>
</Stack>
</Paper>
)}
</Box>
{/* ✅ 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<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>
)}
{/* ✅ 保留:Submit All Scanned Button */}
<Button
variant="contained"
color="success"
onClick={handleSubmitAllScanned}
disabled={scannedItemsCount === 0 || isSubmittingAll}
sx={{ minWidth: '160px' }}
>
{isSubmittingAll ? (
<>
<CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
{t("Submitting...")}
</>
) : (
`${t("Submit All Scanned")} (${scannedItemsCount})`
)}
</Button>
</Box>
</Box>
{qrScanError && !qrScanSuccess && (
<Alert severity="error" sx={{ mb: 2 }}>
{t("QR code does not match any item in current orders.")}
</Alert>
)}
{qrScanSuccess && (
<Alert severity="success" sx={{ mb: 2 }}>
{t("QR code verified.")}
</Alert>
)}

<TableContainer component={Paper}>
<Table>
<TableHead>


+ 76
- 17
src/components/ProductionProcess/MachineScanner.tsx View File

@@ -19,6 +19,9 @@ interface MachineScannerProps {
machines: Machine[];
onMachinesChange: (machines: Machine[]) => void;
error?: string;
isActive?: boolean;
onActivate?: () => void;
onDeactivate?: () => void;
}

const machineDatabase: Machine[] = [
@@ -33,6 +36,9 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
machines,
onMachinesChange,
error,
isActive=false,
onActivate,
onDeactivate,
}) => {
const [scanningMode, setScanningMode] = useState<boolean>(false);
const [scanError, setScanError] = useState<string | null>(null);
@@ -41,6 +47,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({

const startScanning = (): void => {
setScanningMode(true);
onActivate?.();
setTimeout(() => {
if (machineScanRef.current) {
machineScanRef.current.focus();
@@ -50,34 +57,86 @@ const MachineScanner: React.FC<MachineScannerProps> = ({

const stopScanning = (): void => {
setScanningMode(false);
onDeactivate?.();
};

const handleMachineScan = async (
e: React.KeyboardEvent<HTMLInputElement>,
): Promise<void> => {
const target = e.target as HTMLInputElement;
const scannedCodeJSON = target.value.trim();
let scannedInput = target.value.trim();

if (e.key === "Enter" || scannedCodeJSON.endsWith("}")) {
const scannedObj: MachineQrCode = JSON.parse(scannedCodeJSON);
if (e.key === "Enter" || scannedInput.endsWith("}")) {
console.log("Raw machine input:", scannedInput);
try {
let machineCode: string;
// ✅ 尝试解析 JSON
try {
const scannedObj: MachineQrCode = JSON.parse(scannedInput);
machineCode = scannedObj.code;
} catch (jsonError) {
// ✅ 如果不是 JSON,尝试从花括号中提取
const match = scannedInput.match(/\{([^}?]+)\??}?/);
if (match && match[1]) {
machineCode = match[1].trim();
console.log("Extracted machine code from braces:", machineCode);
} else {
// ✅ 如果没有花括号,直接使用输入值
machineCode = scannedInput.replace(/[{}?]/g, '').trim();
console.log("Using plain machine code:", machineCode);
}
}

const response = await isCorrectMachineUsed(scannedObj?.code);
if (!machineCode) {
setScanError("Invalid input format");
return;
}

if (response.message === "Success") {
const isAlreadyAdded = machines.some(
(m) => m.code === response.entity.code,
);
// ✅ 首先尝试从 API 获取
const response = await isCorrectMachineUsed(machineCode);

if (!isAlreadyAdded) {
onMachinesChange([...machines, response.entity]);
}
if (response.message === "Success") {
const isAlreadyAdded = machines.some(
(m) => m.code === response.entity.code,
);

target.value = "";
// stopScanning();
} else {
alert("Machine not found. Please check the code and try again.");
if (!isAlreadyAdded) {
onMachinesChange([...machines, response.entity]);
}

target.value = "";
setScanError(null);
} else {
// ✅ 如果 API 失败,尝试从本地默认数据查找
const localMachine = machineDatabase.find(
(m) => m.code.toLowerCase() === machineCode.toLowerCase()
);
if (localMachine) {
const isAlreadyAdded = machines.some(
(m) => m.code === localMachine.code
);
if (!isAlreadyAdded) {
onMachinesChange([...machines, localMachine]);
}
target.value = "";
setScanError(null);
console.log("✅ Used local machine data:", localMachine);
} else {
setScanError(
"Machine not found. Please check the code and try again."
);
target.value = "";
}
}
} catch (error) {
console.error("Error processing machine scan:", error);
setScanError(
"An error occurred while checking the operator. Please try again.",
"An error occurred while checking the machine. Please try again."
);
target.value = "";
}
@@ -123,7 +182,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
<TextField
inputRef={machineScanRef}
type="text"
onKeyPress={handleMachineScan}
onKeyDown={handleMachineScan}
fullWidth
label="Scan machine QR code or enter manually..."
variant="outlined"


+ 41
- 6
src/components/ProductionProcess/OperatorScanner.tsx View File

@@ -18,20 +18,32 @@ interface OperatorScannerProps {
operators: Operator[];
onOperatorsChange: (operators: Operator[]) => void;
error?: string;
isActive?: boolean;
onActivate?: () => void;
onDeactivate?: () => void;
}

const OperatorScanner: React.FC<OperatorScannerProps> = ({
operators,
onOperatorsChange,
error,
isActive=false,
onActivate,
onDeactivate,
}) => {
const [scanningMode, setScanningMode] = useState<boolean>(false);
const [scanError, setScanError] = useState<string | null>(null);
const operatorScanRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!isActive && scanningMode) {

stopScanning();
}
}, [isActive]);
const startScanning = (): void => {
setScanningMode(true);
setScanError(null);
onActivate?.();
setTimeout(() => {
if (operatorScanRef.current) {
operatorScanRef.current.focus();
@@ -42,20 +54,44 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({
const stopScanning = (): void => {
setScanningMode(false);
setScanError(null);
onDeactivate?.();
};

const handleOperatorScan = async (
e: React.KeyboardEvent<HTMLInputElement>,
): Promise<void> => {
const target = e.target as HTMLInputElement;
const usernameJSON: string = target.value.trim();
let usernameInput: string = target.value.trim();

if (e.key === "Enter" || usernameJSON.endsWith("}")) {
console.log(usernameJSON);
if (e.key === "Enter" || usernameInput.endsWith("}")) {
console.log("Raw input:", usernameInput);
try {
const usernameObj: OperatorQrCode = JSON.parse(usernameJSON);
let username: string;
// ✅ 尝试解析 JSON
try {
const usernameObj: OperatorQrCode = JSON.parse(usernameInput);
username = usernameObj.username;
} catch (jsonError) {
// ✅ 如果不是 JSON,尝试从花括号中提取
const match = usernameInput.match(/\{([^}?]+)\??}?/);
if (match && match[1]) {
username = match[1].trim();
console.log("Extracted username from braces:", username);
} else {
// ✅ 如果没有花括号,直接使用输入值
username = usernameInput.replace(/[{}?]/g, '').trim();
console.log("Using plain username:", username);
}
}

if (!username) {
setScanError("Invalid input format");
return;
}

const response = await isOperatorExist(usernameObj.username);
const response = await isOperatorExist(username);

if (response.message === "Success") {
const isAlreadyAdded = operators.some(
@@ -66,7 +102,6 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({
}
target.value = "";
setScanError(null);
// stopScanning();
} else {
setScanError(
"Operator not found. Please check the ID and try again.",


+ 17
- 0
src/components/ProductionProcess/ProductionRecordingModal.tsx View File

@@ -1,6 +1,7 @@
"use client";
import React from "react";
import { X, Save } from "@mui/icons-material";
import { useState } from "react";
import {
Dialog,
DialogTitle,
@@ -56,6 +57,16 @@ const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({
const watchedOperators = watch("operators");
const watchedMachines = watch("machines");
const watchedMaterials = watch("materials");
const [activeScannerType, setActiveScannerType] = useState<'operator' | 'machine' | 'material' | null>(null);


const handleScannerActivate = (scannerType: 'operator' | 'machine' | 'material') => {
setActiveScannerType(scannerType);
};

const handleScannerDeactivate = () => {
setActiveScannerType(null);
};

const validateForm = (): boolean => {
let isValid = true;
@@ -206,6 +217,9 @@ const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({
}
}}
error={errors.operators?.message}
isActive={activeScannerType === 'operator'}
onActivate={() => handleScannerActivate('operator')}
onDeactivate={handleScannerDeactivate}
/>

{/* Machine Scanner */}
@@ -218,6 +232,9 @@ const ProductionRecordingModal: React.FC<ProductionRecordingModalProps> = ({
}
}}
error={errors.machines?.message}
isActive={activeScannerType === 'machine'}
onActivate={() => handleScannerActivate('machine')}
onDeactivate={handleScannerDeactivate}
/>

{/* Material Lot Scanner */}


+ 11
- 0
src/i18n/zh/pickOrder.json View File

@@ -17,8 +17,19 @@
"Delivery Order Code(s)": "送貨單編號",
"Start Success": "開始成功",
"Truck Lance Code": "車牌號碼",
"Pick Order Codes": "提料單編號",
"Pick Order Lines": "提料單行數",
"Delivery Order Codes": "送貨單編號",
"Delivery Order Lines": "送貨單行數",
"Lines Per Pick Order": "每提料單行數",
"Pick Orders Details": "提料單詳情",
"Lines": "行數",
"Finsihed good items": "成品項目",
"kinds": "款",
"Completed Date": "完成日期",
"Completed Time": "完成時間",
"Delivery Order": "送貨單",
"items": "項目",
"Select Pick Order:": "選擇提料單:",
"⚠️ No Stock Available": "⚠️ 沒有庫存",
"Start Fail": "開始失敗",


Loading…
Cancel
Save