Quellcode durchsuchen

reset function of po picking

reset-do-picking-order
PC-20260115JRSN\Administrator vor 4 Tagen
Ursprung
Commit
c6e499a557
4 geänderte Dateien mit 262 neuen und 161 gelöschten Zeilen
  1. +34
    -0
      src/app/api/do/actions.tsx
  2. +59
    -2
      src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx
  3. +32
    -28
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  4. +137
    -131
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx

+ 34
- 0
src/app/api/do/actions.tsx Datei anzeigen

@@ -408,6 +408,40 @@ export async function printDNLabels(request: PrintDNLabelsRequest){

return { success: true, message: "Print job sent successfully (labels)"} as PrintDeliveryNoteResponse
}

export interface ResetDoPickOrderResponse {
success: boolean;
message?: string;
}

export async function resetDoPickOrderToNonPick(doPickOrderRecordId: number): Promise<ResetDoPickOrderResponse> {
const params = new URLSearchParams();
params.append("doPickOrderRecordId", doPickOrderRecordId.toString());

try {
const response = await serverFetchWithNoContent(
`${BASE_API_URL}/doPickOrder/reset-to-non-pick?${params.toString()}`,
{
method: "POST",
},
);

if (response) {
return { success: true };
}

return {
success: false,
message: "Failed to reset DO pick order to non-pick state.",
};
} catch (error) {
console.error("Error in resetDoPickOrderToNonPick:", error);
return {
success: false,
message: "Error occurred while resetting DO pick order to non-pick state.",
};
}
}
export interface Check4FTruckBatchResponse {
hasProblem: boolean;
problems: ProblemDoDto[];


+ 59
- 2
src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx Datei anzeigen

@@ -20,15 +20,22 @@ import {
Paper,
CircularProgress,
TablePagination,
Chip
Chip,
Button,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { arrayToDayjs } from '@/app/utils/formatUtil';
import { fetchTicketReleaseTable, getTicketReleaseTable } from '@/app/api/do/actions';
import { fetchTicketReleaseTable, getTicketReleaseTable, resetDoPickOrderToNonPick } from '@/app/api/do/actions';
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { AUTH } from "@/authorities";
import Swal from 'sweetalert2';

const FGPickOrderTicketReleaseTable: React.FC = () => {
const { t } = useTranslation("ticketReleaseTable");
const { data: session } = useSession() as { data: SessionWithTokens | null };
const abilities = (session?.abilities ?? []) as string[];
const [selectedDate, setSelectedDate] = useState<string>("today");
const [selectedFloor, setSelectedFloor] = useState<string>("");
const [selectedStatus, setSelectedStatus] = useState<string>("released");
@@ -280,6 +287,7 @@ useEffect(() => {
</TableCell>
<TableCell>{t("Handler Name")}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%', whiteSpace: 'nowrap' }}>{t("Number of FG Items (Order Item(s) Count)")}</TableCell>
<TableCell sx={{ minWidth: 140 }}>{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -378,6 +386,55 @@ useEffect(() => {
</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%' }}>{row.handlerName ?? 0}</TableCell>
<TableCell align="right" sx={{ minWidth: 100, width: '8%' }}>{row.numberOfFGItems ?? 0}</TableCell>
<TableCell>
{abilities.includes(AUTH.ADMIN) && (
<Button
variant="outlined"
color="warning"
size="small"
disabled={!row.id}
onClick={async () => {
const result = await Swal.fire({
title: t("Reset to non-pick state?"),
text: t("This will move the order back to non-pick state so it can be processed again."),
icon: "warning",
showCancelButton: true,
confirmButtonText: t("Confirm"),
cancelButtonText: t("Cancel"),
confirmButtonColor: "#8dba00",
cancelButtonColor: "#F04438",
});

if (!result.isConfirmed || !row.id) {
return;
}

const resetResult = await resetDoPickOrderToNonPick(row.id);

if (resetResult.success) {
await loadData();
Swal.fire({
position: "bottom-end",
icon: "success",
text: t("Order has been reset to non-pick state."),
showConfirmButton: false,
timer: 1500,
});
} else {
Swal.fire({
icon: "error",
title: t("Error"),
text: resetResult.message || t("Failed to reset order."),
confirmButtonText: t("Confirm"),
confirmButtonColor: "#8dba00",
});
}
}}
>
{t("Reset to Non-pick")}
</Button>
)}
</TableCell>
</TableRow>
);
})


+ 32
- 28
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx Datei anzeigen

@@ -771,34 +771,38 @@ if (showDetailView && selectedDoPickOrder) {
>
{t("View Details")}
</Button>
<>
<Button
variant="contained"
onClick={() => handleDN(
doPickOrder.doPickOrderRecordId
)}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
onClick={() => handleDNandLabel(
doPickOrder.doPickOrderRecordId
)}
>
{t("Print DN & Label")}
</Button>
<Button
variant="contained"
onClick={() => handleLabel(
doPickOrder.doPickOrderRecordId
)}
>
{t("Print Label")}
</Button>
</>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
onClick={() =>
handleDN(
doPickOrder.doPickOrderRecordId,
)
}
>
{t("Print Pick Order")}
</Button>
<Button
variant="contained"
onClick={() =>
handleDNandLabel(
doPickOrder.doPickOrderRecordId,
)
}
>
{t("Print DN & Label")}
</Button>
<Button
variant="contained"
onClick={() =>
handleLabel(
doPickOrder.doPickOrderRecordId,
)
}
>
{t("Print Label")}
</Button>
</Stack>
</CardActions>
</Card>
))}


+ 137
- 131
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Datei anzeigen

@@ -581,14 +581,29 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
if (combinedLotData.length === 0) {
return { completed: 0, total: 0 };
}
const nonPendingCount = combinedLotData.filter(lot => {
const status = lot.stockOutLineStatus?.toLowerCase();
return status !== 'pending';

// Use same logic as scannedItemsCount: count only lines that are actually scanned/submittable
const scannedCount = combinedLotData.filter(lot => {
const status = lot.stockOutLineStatus;
if (!status) return false;

if (lot.noLot === true) {
return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
);
}

return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
);
}).length;
return {
completed: nonPendingCount,
completed: scannedCount,
total: combinedLotData.length
};
}, [combinedLotData]);
@@ -2740,46 +2755,33 @@ const handleSubmitAllScanned = useCallback(async () => {
try {
// 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配)
const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
// 1. 需求数量
// 1. 需求数量:优先用 lot.requiredQty,没有就用 pickOrderLineRequiredQty
const requiredQty =
Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
// 2. 当前已经拣到的数量(数据库里的 qty)
// 2. 当前已经拣到的数量(数据库里对应 stock_out_line.qty)
const currentActualPickQty = Number(lot.actualPickQty || 0);
// 🔹 判断是否走“只改状态模式”
// 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成,
// 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。
const onlyComplete = lot.stockOutLineStatus === "partially_completed";
// lot.stockOutLineStatus === "partially_completed" && false === true;
let targetActual: number;
let newStatus: string;
if (onlyComplete) {
// ✅ 只改状态:目标数量 = 当前数量,不再补拣
targetActual = currentActualPickQty;

// 3. 还需要拣多少:不能为负数
const remainingQty = Math.max(0, requiredQty - currentActualPickQty);

// 4. 本次批量提交后的目标累计值 = 当前 + 剩余
const cumulativeQty = currentActualPickQty + remainingQty;

// 5. 根据“目标累计值是否达到需求”决定状态
let newStatus = "partially_completed";
if (requiredQty > 0 && cumulativeQty >= requiredQty) {
newStatus = "completed";
} else {
// ✅ 补拣模式:把剩余全部拣完
const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
const cumulativeQty = currentActualPickQty + remainingQty;
targetActual = cumulativeQty;
newStatus = "partially_completed";
if (requiredQty > 0 && cumulativeQty >= requiredQty) {
newStatus = "completed";
}
}

return {
stockOutLineId: Number(lot.stockOutLineId) || 0,
pickOrderLineId: Number(lot.pickOrderLineId),
// ⚠️ 这里按你现在的写法是用 lot.lotId,当心是否真的是 inventoryLotLineId
inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
requiredQty,
// 后端用 targetActual - 当前 qty 算增量,onlyComplete 时就是 0
actualPickQty: targetActual,
// 传“目标累计值”,后端会用它减去当前数据库里的 qty 得到增量
actualPickQty: cumulativeQty,
stockOutLineStatus: newStatus,
pickOrderConsoCode: String(lot.pickOrderConsoCode || ""),
noLot: Boolean(lot.noLot === true),
@@ -2838,36 +2840,29 @@ const handleSubmitAllScanned = useCallback(async () => {
}
}, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount]);

// Calculate scanned items count
// Calculate scanned items count (should match handleSubmitAllScanned filter logic)
const scannedItemsCount = useMemo(() => {
const filtered = combinedLotData.filter(lot => {
const status = lot.stockOutLineStatus;
// ✅ 与 handleSubmitAllScanned 完全保持一致
if (lot.noLot === true) {
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
}
return status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE';
});
// 添加调试日志
const noLotCount = filtered.filter(l => l.noLot === true).length;
const normalCount = filtered.filter(l => l.noLot !== true).length;
console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`);
console.log(`📊 All items breakdown:`, {
total: combinedLotData.length,
noLot: combinedLotData.filter(l => l.noLot === true).length,
normal: combinedLotData.filter(l => l.noLot !== true).length
});
return filtered.length;
}, [combinedLotData]);
// Calculate scanned items count (must match handleSubmitAllScanned filter logic)
const scannedItemsCount = useMemo(() => {
const filtered = combinedLotData.filter(lot => {
const status = lot.stockOutLineStatus;
if (!status) return false;

if (lot.noLot === true) {
return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
);
}

return (
status === 'checked' ||
status === 'partially_completed' ||
status === 'PARTIALLY_COMPLETE'
);
});

return filtered.length;
}, [combinedLotData]);
/*
// ADD THIS: Auto-stop scan when no data available
useEffect(() => {
@@ -3183,71 +3178,82 @@ paginatedData.map((lot, index) => {
</TableCell>
<TableCell align="center">
{(() => {
const status = lot.stockOutLineStatus?.toLowerCase();
const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
const isNoLot = !lot.lotNo;
// rejected lot:显示红色勾选(已扫描但被拒绝)
if (isRejected && !isNoLot) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'error.main',
'&.Mui-checked': { color: 'error.main' },
transform: 'scale(1.3)',
}}
/>
</Box>
);
}
// 正常 lot:已扫描(checked/partially_completed/completed)
if (!isNoLot && status !== 'pending' && status !== 'rejected') {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'success.main',
'&.Mui-checked': { color: 'success.main' },
transform: 'scale(1.3)',
}}
/>
</Box>
);
}
// noLot 且已完成/部分完成:显示红色勾选
if (isNoLot && (status === 'partially_completed' || status === 'completed')) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Checkbox
checked={true}
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'error.main',
'&.Mui-checked': { color: 'error.main' },
transform: 'scale(1.3)',
}}
/>
</Box>
);
}
return null;
})()}
</TableCell>
{(() => {
const rawStatus = lot.stockOutLineStatus;
const status = rawStatus?.toLowerCase();
const isRejected = status === "rejected" || lot.lotAvailability === "rejected";
const isNoLot = !lot.lotNo;

// rejected lot:显示红色勾选(已扫描但被拒绝)
if (isRejected && !isNoLot) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<Checkbox
checked
disabled
readOnly
size="large"
sx={{
color: "error.main",
"&.Mui-checked": { color: "error.main" },
transform: "scale(1.3)",
}}
/>
</Box>
);
}

// 正常 lot:已扫描(只有在明确的 scanned 状态时显示勾)
const isScannedNormal =
!isNoLot &&
!!status &&
(status === "checked" ||
status === "partially_completed" ||
status === "partially_complete" ||
status === "completed" ||
status === "complete");

if (isScannedNormal) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<Checkbox
checked
disabled
readOnly
size="large"
sx={{
color: "success.main",
"&.Mui-checked": { color: "success.main" },
transform: "scale(1.3)",
}}
/>
</Box>
);
}

// noLot 且已完成/部分完成:显示红色勾选
if (isNoLot && (status === "partially_completed" || status === "completed")) {
return (
<Box sx={{ display: "flex", justifyContent: "center", alignItems: "center" }}>
<Checkbox
checked
disabled
readOnly
size="large"
sx={{
color: "error.main",
"&.Mui-checked": { color: "error.main" },
transform: "scale(1.3)",
}}
/>
</Box>
);
}

// 其他情况(包括 status 为 null/undefined):不显示勾
return null;
})()}
</TableCell>
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{(() => {


Laden…
Abbrechen
Speichern