kelvin.yau 1 month ago
parent
commit
b9817491ab
14 changed files with 441 additions and 176 deletions
  1. +110
    -7
      src/app/api/pickOrder/actions.ts
  2. +19
    -7
      src/components/FinishedGoodSearch/FGPickOrderInfoCard.tsx
  3. +3
    -0
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  4. +39
    -28
      src/components/FinishedGoodSearch/GoodPickExecution.tsx
  5. +154
    -84
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  6. +93
    -36
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  7. +4
    -2
      src/components/JoSearch/JoSearch.tsx
  8. +6
    -3
      src/components/JoSearch/JoSearchWrapper.tsx
  9. +4
    -4
      src/components/Logo/Logo.tsx
  10. +1
    -1
      src/components/MailField/MailField.css
  11. +1
    -1
      src/components/StockIn/FgStockInForm.tsx
  12. +1
    -1
      src/components/StockIn/StockInForm.tsx
  13. +4
    -1
      src/i18n/zh/pickOrder.json
  14. +2
    -1
      src/i18n/zh/purchaseOrder.json

+ 110
- 7
src/app/api/pickOrder/actions.ts View File

@@ -246,6 +246,15 @@ export interface UpdateSuggestedLotLineIdRequest {
newLotLineId: number;
}
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 数量
// ✅ 保留原有字段用于向后兼容(显示第一个 pick order)
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
@@ -266,6 +275,37 @@ export interface FGPickOrderResponse {
storeId: string;
qrCodeData: number;
}
export interface DoPickOrderDetail {
doPickOrder: {
id: number;
store_id: string;
ticket_no: string;
ticket_status: string;
truck_id: number;
truck_departure_time: string;
shop_id: number;
handled_by: number | null;
loading_sequence: number;
ticket_release_time: string | null;
TruckLanceCode: string;
ShopCode: string;
ShopName: string;
RequiredDeliveryDate: string;
};
pickOrders: Array<{
pick_order_id: number;
pick_order_code: string;
do_order_id: number;
delivery_order_code: string;
consoCode: string;
status: string;
targetDate: string;
}>;
selectedPickOrderId: number;
lotDetails: any[]; // 使用现有的 lot detail 结构
pickOrderCodes?: string;
deliveryNos?: string;
}
export interface AutoAssignReleaseByStoreRequest {
userId: number;
storeId: string; // "2/F" | "4/F"
@@ -280,12 +320,17 @@ export interface UpdateDoPickOrderHideStatusRequest {
}
export interface CompletedDoPickOrderResponse {
id: number;
doPickOrderRecordId: number; // ✅ 新增
pickOrderId: number;
pickOrderIds: number[]; // ✅ 新增:所有 pick order IDs
pickOrderCode: string;
pickOrderCodes: string; // ✅ 新增:所有 pick order codes (逗号分隔)
pickOrderConsoCode: string;
pickOrderStatus: string;
deliveryOrderId: number;
deliveryOrderIds: number[]; // ✅ 新增:所有 delivery order IDs
deliveryNo: string;
deliveryNos: string; // ✅ 新增:所有 delivery order codes (逗号分隔)
deliveryDate: string;
shopId: number;
shopCode: string;
@@ -295,6 +340,7 @@ export interface CompletedDoPickOrderResponse {
shopPoNo: string;
numberOfCartons: number;
truckLanceCode: string;
DepartureTime: string; // ✅ 新增
storeId: string;
completedDate: string;
fgPickOrders: FGPickOrderResponse[];
@@ -386,6 +432,20 @@ export interface LaneBtn {
unassigned: number;
total: number;
}
export const fetchDoPickOrderDetail = async (
doPickOrderId: number,
selectedPickOrderId?: number
): Promise<DoPickOrderDetail> => {
const url = selectedPickOrderId
? `${BASE_API_URL}/pickOrder/do-pick-order-detail/${doPickOrderId}?selectedPickOrderId=${selectedPickOrderId}`
: `${BASE_API_URL}/pickOrder/do-pick-order-detail/${doPickOrderId}`;
const response = await serverFetchJson<DoPickOrderDetail>(url, {
method: "GET",
});
return response;
};
export const updatePickExecutionIssueStatus = async (
data: UpdatePickExecutionIssueRequest
): Promise<PostPickOrderResponse> => {
@@ -739,6 +799,40 @@ interface SuggestionWithStatus {
stockOutLineQty?: number;
suggestionStatus: 'active' | 'completed' | 'rejected' | 'in_progress' | 'unknown';
}
// 在 actions.ts 中修改接口定义
export interface FGPickOrderHierarchicalResponse {
fgInfo: {
doPickOrderId: number;
ticketNo: string;
storeId: string;
shopCode: string;
shopName: string;
truckLanceCode: string;
departureTime: string;
};
pickOrders: Array<{
pickOrderId: number;
pickOrderCode: string;
doOrderId: number;
deliveryOrderCode: string;
consoCode: string;
status: string;
targetDate: string;
pickOrderLines: Array<{
id: number;
requiredQty: number;
status: string;
item: {
id: number;
code: string;
name: string;
uomCode: string;
uomDesc: string;
};
lots: Array<any>; // 可以是空数组
}>;
}>;
}
export interface CheckCompleteResponse {
id: number | null;
name: string;
@@ -823,23 +917,32 @@ export const fetchAllPickOrderLotsHierarchical = cache(async (userId: number): P
};
}
});
export const fetchLotDetailsByPickOrderId = async (pickOrderId: number): Promise<any[]> => {
export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId: number): Promise<{
fgInfo: any;
pickOrders: any[];
}> => {
try {
console.log("🔍 Fetching lot details for pickOrderId:", pickOrderId);
console.log("🔍 Fetching lot details for doPickOrderRecordId:", doPickOrderRecordId);
const data = await serverFetchJson<any[]>(
`${BASE_API_URL}/pickOrder/lot-details-by-pick-order/${pickOrderId}`,
const data = await serverFetchJson<{
fgInfo: any;
pickOrders: any[];
}>(
`${BASE_API_URL}/pickOrder/lot-details-by-do-pick-order-record/${doPickOrderRecordId}`,
{
method: 'GET',
next: { tags: ["pickorder"] },
}
);
console.log("✅ Fetched lot details for pickOrderId:", data);
console.log("✅ Fetched hierarchical lot details:", data);
return data;
} catch (error) {
console.error("❌ Error fetching lot details for pickOrderId:", error);
return [];
console.error("❌ Error fetching lot details:", error);
return {
fgInfo: null,
pickOrders: []
};
}
};
// Update the existing function to use the non-auto-assign endpoint


+ 19
- 7
src/components/FinishedGoodSearch/FGPickOrderInfoCard.tsx View File

@@ -2,14 +2,22 @@

import { Box, Card, CardContent, Grid, TextField, Stack } from "@mui/material";
import { useTranslation } from "react-i18next";
import { FGPickOrderResponse } from "@/app/api/pickOrder/actions";
import { FGPickOrderResponse, DoPickOrderDetail } from "@/app/api/pickOrder/actions";

interface Props {
fgOrder: FGPickOrderResponse;
doPickOrderDetail?: DoPickOrderDetail | null;
}

const FGPickOrderInfoCard: React.FC<Props> = ({ fgOrder }) => {
const FGPickOrderInfoCard: React.FC<Props> = ({ fgOrder, doPickOrderDetail }) => {
const { t } = useTranslation("pickOrder");
if (!fgOrder) {
return null;
}
const pickOrderCodes = fgOrder.pickOrderCodes || "";
const deliveryOrderCodes = fgOrder.deliveryNos || "";

return (
<Card sx={{ display: "block", mb: 2 }}>
@@ -17,21 +25,25 @@ const FGPickOrderInfoCard: React.FC<Props> = ({ fgOrder }) => {
<Box>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<Grid item xs={6}>
<TextField
value={fgOrder.pickOrderCode || ""}
label={t("Pick Order Code")}
value={pickOrderCodes || ""} // ✅ 显示所有 pick order codes
label={t("Pick Order Code(s)")} // ✅ 修改标签
fullWidth
disabled={true}
multiline={pickOrderCodes.includes(',')} // ✅ 如果有多个代码,使用多行
rows={pickOrderCodes.includes(',') ? 2 : 1}
/>
</Grid>
<Grid item xs={6}>
<TextField
value={fgOrder.deliveryNo || ""}
label={t("Delivery No")}
value={deliveryOrderCodes || ""} // ✅ 显示所有 delivery order codes
label={t("Delivery Order Code(s)")} // ✅ 修改标签
fullWidth
disabled={true}
multiline={deliveryOrderCodes.includes(',')} // ✅ 如果有多个代码,使用多行
rows={deliveryOrderCodes.includes(',') ? 2 : 1}
/>
</Grid>


+ 3
- 0
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx View File

@@ -22,6 +22,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
const [isLoadingSummary, setIsLoadingSummary] = useState(false);
const [isAssigning, setIsAssigning] = useState(false);
//const [selectedDate, setSelectedDate] = useState<string>("today");
const [selectedDate, setSelectedDate] = useState<string>("today");

const loadSummaries = useCallback(async () => {
@@ -132,6 +133,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
<Box sx={{ maxWidth: 300, mb: 2 }}>
<FormControl fullWidth size="small">
<InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
<Select
labelId="date-select-label"
id="date-select"
@@ -154,6 +156,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
{t("Day After Tomorrow")} ({getDateLabel(2)})
</MenuItem>
</Select>
</FormControl>
</Box>



+ 39
- 28
src/components/FinishedGoodSearch/GoodPickExecution.tsx View File

@@ -32,7 +32,9 @@ import {
AutoAssignReleaseResponse,
checkPickOrderCompletion,
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode
checkAndCompletePickOrderByConsoCode,
fetchDoPickOrderDetail,
DoPickOrderDetail,
} from "@/app/api/pickOrder/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
@@ -50,7 +52,8 @@ import { fetchStockInLineInfo } from "@/app/api/po/actions";
import GoodPickExecutionForm from "./GoodPickExecutionForm";
import FGPickOrderCard from "./FGPickOrderCard";
import FinishedGoodFloorLanePanel from "./FinishedGoodFloorLanePanel";
import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
import GoodPickExecutiondetail from "./GoodPickExecutiondetail";
interface Props {
filterArgs: Record<string, any>;
onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void;
@@ -322,7 +325,9 @@ const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange }) =>
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const [qrScanInput, setQrScanInput] = useState<string>('');
const [qrScanError, setQrScanError] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
@@ -352,25 +357,28 @@ const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange }) =>
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
const fetchFgPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
// 在 GoodPickExecutiondetail.tsx 中修改 fetchFgPickOrdersData
// 修改 fetchFgPickOrdersData 函数:
const fetchFgPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
setFgPickOrdersLoading(true);
try {
const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
setFgPickOrdersLoading(true);
try {
// ✅ 简化:直接使用 userId 调用 API,不需要循环
const fgPickOrders = await fetchFGPickOrdersByUserId(currentUserId);
setFgPickOrders(fgPickOrders);
onFgPickOrdersChange?.(fgPickOrders);
console.log("✅ Fetched FG pick orders for user:", fgPickOrders);
} catch (error) {
console.error("❌ Error fetching FG pick orders:", error);
setFgPickOrders([]);
onFgPickOrdersChange?.([]);
} finally {
setFgPickOrdersLoading(false);
}
}, [currentUserId, onFgPickOrdersChange]);
console.log("🔍 DEBUG: Fetched FG pick orders:", fgPickOrders);
console.log("🔍 DEBUG: First order numberOfPickOrders:", fgPickOrders[0]?.numberOfPickOrders);
setFgPickOrders(fgPickOrders);

} catch (error) {
console.error("❌ Error fetching FG pick orders:", error);
setFgPickOrders([]);
} finally {
setFgPickOrdersLoading(false);
}
}, [currentUserId, selectedPickOrderId]);
// ✅ 简化:移除复杂的 useEffect 依赖
useEffect(() => {
@@ -972,13 +980,16 @@ return (
) : (
// ✅ 有活动订单,显示 FG 订单信息
<Box>
{fgPickOrders.map((fgOrder) => (
<FGPickOrderInfoCard
key={fgOrder.pickOrderId}
fgOrder={fgOrder}
/>
))}
</Box>
{fgPickOrders.map((fgOrder) => (
<Box key={fgOrder.pickOrderId} sx={{ mb: 2 }}>
<FGPickOrderInfoCard
fgOrder={fgOrder}
/>

</Box>
))}
</Box>
)}
{/* Modals */}


+ 154
- 84
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx View File

@@ -44,7 +44,7 @@ import {
fetchCompletedDoPickOrders,
CompletedDoPickOrderResponse,
CompletedDoPickOrderSearchParams,
fetchLotDetailsByPickOrderId
fetchLotDetailsByDoPickOrderRecordId
} from "@/app/api/pickOrder/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
@@ -407,30 +407,54 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
setSelectedDoPickOrder(doPickOrder);
setShowDetailView(true);
// ✅ 修复:使用新的 API 根据 pickOrderId 获取 lot 详情
try {
const lotDetails = await fetchLotDetailsByPickOrderId(doPickOrder.pickOrderId);
setDetailLotData(lotDetails);
console.log("✅ Loaded detail lot data for pick order:", doPickOrder.pickOrderCode, lotDetails);
// ✅ 使用 doPickOrderRecordId 而不是 pickOrderId
const hierarchicalData = await fetchLotDetailsByDoPickOrderRecordId(doPickOrder.doPickOrderRecordId);
console.log("✅ Loaded hierarchical lot data:", hierarchicalData);
// ✅ 触发打印按钮状态更新 - 基于详情数据
const allCompleted = lotDetails.length > 0 && lotDetails.every(lot =>
// ✅ 转换为平铺格式
const flatLotData: any[] = [];
if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) {
hierarchicalData.pickOrders.forEach((po: any) => {
po.pickOrderLines?.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
line.lots.forEach((lot: any) => {
flatLotData.push({
pickOrderCode: po.pickOrderCode,
itemCode: line.item.code,
itemName: line.item.name,
lotNo: lot.lotNo,
location: lot.location,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
processingStatus: lot.processingStatus,
stockOutLineStatus: lot.stockOutLineStatus
});
});
}
});
});
}
setDetailLotData(flatLotData);
// ✅ 计算完成状态
const allCompleted = flatLotData.length > 0 && flatLotData.every(lot =>
lot.processingStatus === 'completed'
);
// ✅ 发送事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件
tabIndex: 2
}
}));
} catch (error) {
} catch (error) { // ✅ 添加 catch 块
console.error("❌ Error loading detail lot data:", error);
setDetailLotData([]);
// ✅ 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -458,86 +482,132 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {


// ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格
if (showDetailView && selectedDoPickOrder) {
return (
<FormProvider {...formProps}>
<Box>
{/* 返回按钮和标题 */}
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button variant="outlined" onClick={handleBackToList}>
{t("Back to List")}
</Button>
<Typography variant="h6">
{t("Pick Order Details")}: {selectedDoPickOrder.pickOrderCode}
</Typography>
</Box>
// ✅ 如果显示详情视图,渲染层级结构
if (showDetailView && selectedDoPickOrder) {
return (
<FormProvider {...formProps}>
<Box>
{/* 返回按钮和标题 */}
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button variant="outlined" onClick={handleBackToList}>
{t("Back to List")}
</Button>
<Typography variant="h6">
{t("Pick Order Details")}: {selectedDoPickOrder.ticketNo}
</Typography>
</Box>

{/* 订单基本信息 */}
<Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
{t("Order Information")}
</Typography>
<Typography variant="body2">
{/* FG 订单基本信息 */}
<Paper sx={{ mb: 2, p: 2 }}>
<Stack spacing={1}>
<Typography variant="subtitle1">
<strong>{t("Shop Name")}:</strong> {selectedDoPickOrder.shopName}
</Typography>
<Typography variant="body2">
<strong>{t("Delivery No")}:</strong> {selectedDoPickOrder.deliveryNo}
<Typography variant="subtitle1">
<strong>{t("Store ID")}:</strong> {selectedDoPickOrder.storeId}
</Typography>
<Typography variant="body2">
<Typography variant="subtitle1">
<strong>{t("Ticket No.")}:</strong> {selectedDoPickOrder.ticketNo}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Truck Lance Code")}:</strong> {selectedDoPickOrder.truckLanceCode}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Completed Date")}:</strong> {dayjs(selectedDoPickOrder.completedDate).format(OUTPUT_DATE_FORMAT)}
</Typography>
</Box>

{/* ✅ 添加数据检查 */}
{detailLotData.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t("No lot details found for this order")}
</Typography>
</Stack>
</Paper>

{/* ✅ 添加:多个 Pick Orders 信息(如果有) */}
{selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && (
<Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("This ticket contains")} {selectedDoPickOrder.pickOrderIds.length} {t("pick orders")}:
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{selectedDoPickOrder.pickOrderCodes?.split(', ').map((code, idx) => (
<Chip
key={idx}
label={code}
size="small"
variant="outlined"
/>
))}
</Box>
) : (
/* 显示完成数据的表格 */
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Pick Order Code")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell>{t("Location")}</TableCell>
<TableCell>{t("Required Qty")}</TableCell>
<TableCell>{t("Actual Pick Qty")}</TableCell>
<TableCell>{t("Submitted Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{detailLotData.map((lot, index) => (
<TableRow key={index}>
<TableCell>{lot.pickOrderCode || 'N/A'}</TableCell>
<TableCell>{lot.itemCode || 'N/A'}</TableCell>
<TableCell>{lot.itemName || 'N/A'}</TableCell>
<TableCell>{lot.lotNo || 'N/A'}</TableCell>
<TableCell>{lot.location || 'N/A'}</TableCell>
<TableCell>{lot.requiredQty || 0}</TableCell>
<TableCell>{lot.actualPickQty || 0}</TableCell>
<TableCell>
<Chip
label={t(lot.processingStatus || 'unknown')}
color={lot.processingStatus === 'completed' ? 'success' : 'default'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
</FormProvider>
);
}
</Paper>
)}

{/* ✅ 数据检查 */}
{detailLotData.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t("No lot details found for this order")}
</Typography>
</Box>
) : (
/* ✅ 按 Pick Order 分组显示 */
<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>
<TableRow>
<TableCell>{t("Index")}</TableCell>
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell>{t("Location")}</TableCell>
<TableCell align="right">{t("Required Qty")}</TableCell>
<TableCell align="right">{t("Actual Pick Qty")}</TableCell>
<TableCell align="center">{t("Status")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(lots as any[]).map((lot: any, index: number) => (
<TableRow key={index}>
<TableCell>{index + 1}</TableCell>
<TableCell>{lot.itemCode || 'N/A'}</TableCell>
<TableCell>{lot.itemName || 'N/A'}</TableCell>
<TableCell>{lot.lotNo || 'N/A'}</TableCell>
<TableCell>{lot.location || 'N/A'}</TableCell>
<TableCell align="right">{lot.requiredQty || 0}</TableCell>
<TableCell align="right">{lot.actualPickQty || 0}</TableCell>
<TableCell align="center">
<Chip
label={t(lot.processingStatus || 'unknown')}
color={lot.processingStatus === 'completed' ? 'success' : 'default'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
))}
</Stack>
)}
</Box>
</FormProvider>
);
}

// ✅ 默认列表视图
return (


+ 93
- 36
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -34,14 +34,18 @@ import {
fetchFGPickOrders, // ✅ Add this import
FGPickOrderResponse,


checkPickOrderCompletion,
fetchAllPickOrderLotsHierarchical,
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode,
updateSuggestedLotLineId,
confirmLotSubstitution, // ✅ 必须添加
confirmLotSubstitution,
fetchDoPickOrderDetail, // ✅ 必须添加
DoPickOrderDetail, // ✅ 必须添加
fetchFGPickOrdersByUserId
} from "@/app/api/pickOrder/actions";

import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
import LotConfirmationModal from "./LotConfirmationModal";
//import { fetchItem } from "@/app/api/settings/item";
@@ -76,7 +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);
@@ -325,7 +329,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const { t } = useTranslation("pickOrder");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const [availablePickOrders, setAvailablePickOrders] = useState<any[]>([]);
const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const currentUserId = session?.id ? parseInt(session.id) : undefined;
@@ -381,6 +385,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
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
@@ -437,11 +444,13 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
return allCompleted;
}, []);
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");
@@ -451,6 +460,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
return;
}
// ✅ 获取新结构的层级数据
// ✅ 获取新结构的层级数据
const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
console.log("✅ Hierarchical data (new structure):", hierarchicalData);
@@ -466,6 +476,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
// ✅ 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
const fgOrder: FGPickOrderResponse = {
doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
ticketNo: hierarchicalData.fgInfo.ticketNo,
storeId: hierarchicalData.fgInfo.storeId,
shopCode: hierarchicalData.fgInfo.shopCode,
@@ -489,17 +500,54 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
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(", ")
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]);
setAvailablePickOrders(hierarchicalData.pickOrders);
// ✅ 构建 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;
@@ -615,14 +663,16 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
});
}
});
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([]);
@@ -1463,8 +1513,9 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
console.error("Error switching pick order:", error);
} finally {
setPickOrderSwitching(false);
}
}, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
}
}, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
const handleStopScan = useCallback(() => {
console.log("⏹️ Stopping manual QR scan...");
setIsManualScanning(false);
@@ -1629,6 +1680,8 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
<strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
</Typography>
</Stack>


</Stack>
</Paper>
@@ -1638,28 +1691,28 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
{/* ✅ FG Info Card */}
{/* ✅ Pick Order Switcher - 放在 FG Info 下面,QR 按钮上面 */}
{availablePickOrders.length > 1 && (
<Box sx={{ mb: 2, mt: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("Select Pick Order:")}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{availablePickOrders.map((po: any) => (
<Chip
key={po.pickOrderId}
label={`${po.pickOrderCode} (${po.deliveryOrderCode})`}
onClick={() => handlePickOrderSwitch(po.pickOrderId)}
color={selectedPickOrderId === po.pickOrderId ? "primary" : "default"}
variant={selectedPickOrderId === po.pickOrderId ? "filled" : "outlined"}
sx={{
cursor: 'pointer',
'&:hover': { backgroundColor: 'primary.light', color: 'white' }
}}
/>
))}
</Box>
</Box>
)}
{doPickOrderDetail && doPickOrderDetail.pickOrders.length > 1 && (
<Box sx={{ mb: 2, mt: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
{t("Select Pick Order:")}
</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>
)}
</Box>
{/* ✅ 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
<Box>
@@ -1967,8 +2020,12 @@ paginatedData.map((lot, index) => {
/>
)}
</FormProvider>

</TestQrCodeProvider>
);
};

export default PickExecution;
export default PickExecution;

+ 4
- 2
src/components/JoSearch/JoSearch.tsx View File

@@ -25,17 +25,19 @@ import dayjs from "dayjs";

import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
printerCombo: PrinterCombo[];
}

type SearchQuery = Partial<Omit<JobOrder, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => {
const { t } = useTranslation("jo");
const router = useRouter()
const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
@@ -426,7 +428,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => {
open={openModal}
onClose={closeNewModal}
inputDetail={modalInfo}
printerCombo={[]}
printerCombo={printerCombo}
// skipQc={true}
/>
</>


+ 6
- 3
src/components/JoSearch/JoSearchWrapper.tsx View File

@@ -3,6 +3,7 @@ import GeneralLoading from "../General/GeneralLoading";
import JoSearch from "./JoSearch";
import { SearchJoResultRequest } from "@/app/api/jo/actions";
import { fetchBomCombo } from "@/app/api/bom";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof GeneralLoading;
@@ -15,12 +16,14 @@ const JoSearchWrapper: React.FC & SubComponents = async () => {
}

const [
bomCombo
bomCombo,
printerCombo
] = await Promise.all([
fetchBomCombo()
fetchBomCombo(),
fetchPrinterCombo()
])
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo}/>
return <JoSearch defaultInputs={defaultInputs} bomCombo={bomCombo} printerCombo={printerCombo}/>
}

JoSearchWrapper.Loading = GeneralLoading;


+ 4
- 4
src/components/Logo/Logo.tsx View File

@@ -14,16 +14,16 @@ const Logo: React.FC<Props> = ({ width, height }) => {
<g
id="svgGroup"
strokeLinecap="round"
fill-rule="evenodd"
font-size="9pt"
fillRule="evenodd"
fontSize="9pt"
stroke="#000"
stroke-width="0.25mm"
strokeWidth="0.25mm"
fill="#000"
// style="stroke:#000;stroke-width:0.25mm;fill:#000"
>
<path
d="M 72.768 0.48 L 75.744 0.48 L 88.224 29.568 L 88.56 29.568 L 101.04 0.48 L 103.92 0.48 L 103.92 34.416 L 101.136 34.416 L 101.136 7.344 L 100.896 7.344 L 89.136 34.416 L 87.504 34.416 L 75.744 7.344 L 75.552 7.344 L 75.552 34.416 L 72.768 34.416 L 72.768 0.48 Z M 137.808 0.48 L 140.784 0.48 L 153.264 29.568 L 153.6 29.568 L 166.08 0.48 L 168.96 0.48 L 168.96 34.416 L 166.176 34.416 L 166.176 7.344 L 165.936 7.344 L 154.176 34.416 L 152.544 34.416 L 140.784 7.344 L 140.592 7.344 L 140.592 34.416 L 137.808 34.416 L 137.808 0.48 Z M 198.72 7.824 L 195.84 7.824 Q 195.456 4.848 193.224 3.696 Q 190.992 2.544 187.344 2.544 Q 183.168 2.544 181.152 4.152 Q 179.136 5.76 179.136 8.88 Q 179.136 10.704 179.832 11.856 Q 180.528 13.008 181.632 13.704 Q 182.736 14.4 183.984 14.808 Q 185.232 15.216 186.288 15.504 L 189.984 16.512 Q 191.376 16.896 193.008 17.472 Q 194.64 18.048 196.104 19.056 Q 197.568 20.064 198.48 21.648 Q 199.392 23.232 199.392 25.584 Q 199.392 28.272 198.096 30.432 Q 196.8 32.592 194.112 33.816 Q 191.424 35.04 187.248 35.04 Q 181.68 35.04 178.704 32.784 Q 175.728 30.528 175.344 26.688 L 178.32 26.688 Q 178.608 28.992 179.808 30.24 Q 181.008 31.488 182.904 31.992 Q 184.8 32.496 187.248 32.496 Q 191.664 32.496 194.088 30.792 Q 196.512 29.088 196.512 25.488 Q 196.512 23.328 195.48 22.08 Q 194.448 20.832 192.744 20.112 Q 191.04 19.392 189.072 18.864 L 184.464 17.616 Q 180.528 16.512 178.392 14.544 Q 176.256 12.576 176.256 9.12 Q 176.256 6.288 177.624 4.248 Q 178.992 2.208 181.512 1.104 Q 184.032 0 187.488 0 Q 190.848 0 193.272 0.984 Q 195.696 1.968 197.112 3.72 Q 198.528 5.472 198.72 7.824 Z M 0 34.416 L 0 0.48 L 19.344 0.48 L 19.344 2.976 L 2.88 2.976 L 2.88 16.176 L 17.76 16.176 L 17.76 18.672 L 2.88 18.672 L 2.88 34.416 L 0 34.416 Z M 108.336 2.976 L 108.336 0.48 L 133.392 0.48 L 133.392 2.976 L 122.304 2.976 L 122.304 34.416 L 119.424 34.416 L 119.424 2.976 L 108.336 2.976 Z M 25.152 34.416 L 25.152 0.48 L 36.48 0.48 Q 40.56 0.48 43.056 1.752 Q 45.552 3.024 46.704 5.328 Q 47.856 7.632 47.856 10.8 Q 47.856 13.968 46.704 16.32 Q 45.552 18.672 43.08 19.968 Q 40.608 21.264 36.576 21.264 L 28.032 21.264 L 28.032 34.416 L 25.152 34.416 Z M 28.032 18.768 L 36.384 18.768 Q 39.744 18.768 41.616 17.784 Q 43.488 16.8 44.232 15 Q 44.976 13.2 44.976 10.8 Q 44.976 8.352 44.232 6.6 Q 43.488 4.848 41.616 3.912 Q 39.744 2.976 36.288 2.976 L 28.032 2.976 L 28.032 18.768 Z M 65.664 18 L 65.664 20.496 L 52.704 20.496 L 52.704 18 L 65.664 18 Z"
vector-effect="non-scaling-stroke"
vectorEffect="non-scaling-stroke"
/>
</g>
</svg>


+ 1
- 1
src/components/MailField/MailField.css View File

@@ -123,7 +123,7 @@

/* Input styles */
/* .tiptap-input {
font-size: 14px;
fontSize: 14px;
font-weight: 500;
line-height: 12px;
} */

+ 1
- 1
src/components/StockIn/FgStockInForm.tsx View File

@@ -67,7 +67,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -0.5rem) scale(1)",
transform: "translate(14px, -9px) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 1
- 1
src/components/StockIn/StockInForm.tsx View File

@@ -60,7 +60,7 @@ const textfieldSx = {
transform: "translate(14px, 1.2rem) scale(1)",
"&.MuiInputLabel-shrink": {
fontSize: 24,
transform: "translate(14px, -0.5rem) scale(1)",
transform: "translate(14px, -9px) scale(1)",
},
// [theme.breakpoints.down("sm")]: {
// fontSize: "1rem",


+ 4
- 1
src/i18n/zh/pickOrder.json View File

@@ -13,11 +13,14 @@
"Assigned To": "已分配",
"Do you want to start?": "確定開始嗎?",
"Start": "開始",
"Pick Order Code(s)": "提料單編號",
"Delivery Order Code(s)": "送貨單編號",
"Start Success": "開始成功",
"Truck Lance Code": "車牌號碼",
"Completed Date": "完成日期",
"Completed Time": "完成時間",

"Select Pick Order:": "選擇提料單:",
"⚠️ No Stock Available": "⚠️ 沒有庫存",
"Start Fail": "開始失敗",
"Start PO": "開始採購訂單",
"Do you want to complete?": "確定完成嗎?",


+ 2
- 1
src/i18n/zh/purchaseOrder.json View File

@@ -164,5 +164,6 @@
"Expiry Date cannot be earlier than Production Date": "到期日不可早於生產日期",
"Production Date must be earlier than Expiry Date": "生產日期必須早於到期日",
"confirm expiry date": "確認到期日",
"Invalid Date": "無效日期"
"Invalid Date": "無效日期",
"Missing QC Template, please contact administrator": "找不到品檢模板,請聯絡管理員"
}

Loading…
Cancel
Save