R53 A1-A6:ERP 集成运维可视化与业务同步状态接入
- ErpIntegrationPage Tab 化:主数据同步 / Outbox 队列管理 / ID 映射查看器 - Outbox 分页查询、详情、重试与统计端点 - IntegrationBinding 分页查询、routePath 推导与采购/资产深链 - 报销/采购/资产详情区接入 ERP 同步状态卡片 - 单测覆盖 Outbox 与 Binding 端点
这个提交包含在:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
@@ -43,6 +44,7 @@ import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -200,6 +202,7 @@ function resolveStageAction(stageKey: PurchaseErpStageKey, stage: PurchaseReques
|
||||
export function PurchaseRequestsPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { permissions } = useAuth();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const canCreate = hasPermission(permissions, "procurement.create");
|
||||
const canManage = hasPermission(permissions, "procurement.manage");
|
||||
@@ -214,7 +217,7 @@ export function PurchaseRequestsPage() {
|
||||
const [legalEntityFilter, setLegalEntityFilter] = useState("");
|
||||
const [costCenterFilter, setCostCenterFilter] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedId, setSelectedId] = useState("");
|
||||
const [selectedId, setSelectedId] = useState(searchParams.get("id") ?? "");
|
||||
const [hydratedDetailId, setHydratedDetailId] = useState("");
|
||||
const [formError, setFormError] = useState("");
|
||||
|
||||
@@ -278,12 +281,20 @@ export function PurchaseRequestsPage() {
|
||||
|
||||
const erpTrackQuery = useQuery({
|
||||
enabled:
|
||||
canIntegrationManage &&
|
||||
Boolean(selectedId) &&
|
||||
(detailQuery.data?.status === "approved" || Boolean(detailQuery.data?.archiveOrder)),
|
||||
queryKey: ["purchase-requests", "erp-track", selectedId],
|
||||
queryFn: () => fetchPurchaseRequestErpTrack(selectedId),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const searchSelectedId = searchParams.get("id") ?? "";
|
||||
if (searchSelectedId && searchSelectedId !== selectedId) {
|
||||
setSelectedId(searchSelectedId);
|
||||
}
|
||||
}, [searchParams, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId) {
|
||||
if (detailQuery.isLoading) {
|
||||
@@ -300,6 +311,23 @@ export function PurchaseRequestsPage() {
|
||||
}
|
||||
}, [detailQuery.isError, detailQuery.isLoading, listQuery.data?.items, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentId = searchParams.get("id") ?? "";
|
||||
if (!selectedId && !currentId) {
|
||||
return;
|
||||
}
|
||||
if (selectedId === currentId) {
|
||||
return;
|
||||
}
|
||||
const next = new URLSearchParams(searchParams);
|
||||
if (selectedId) {
|
||||
next.set("id", selectedId);
|
||||
} else {
|
||||
next.delete("id");
|
||||
}
|
||||
setSearchParams(next, { replace: true });
|
||||
}, [searchParams, selectedId, setSearchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailQuery.data || detailQuery.data.id !== selectedId || hydratedDetailId === detailQuery.data.id) {
|
||||
return;
|
||||
@@ -1240,17 +1268,13 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showArchiveSection ? (
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-base font-semibold text-zinc-900">采购桥接轨道</h4>
|
||||
<p className="text-sm text-zinc-500">展示 Commitment / Accrual / PO/合同摘要三段外部桥接状态,失败后可在此重试。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showArchiveSection && canIntegrationManage ? (
|
||||
<SectionPanel
|
||||
title="ERP 同步状态"
|
||||
description="展示 Commitment / Accrual / PO/合同摘要三段同步状态,失败或未触发时可在此补发或重试。"
|
||||
>
|
||||
{erpTrackQuery.isLoading ? (
|
||||
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">桥接轨道加载中...</p>
|
||||
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">ERP 同步轨道加载中...</p>
|
||||
) : erpTrackQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(erpTrackQuery.error)}</p>
|
||||
) : erpTrack ? (
|
||||
@@ -1297,7 +1321,7 @@ export function PurchaseRequestsPage() {
|
||||
<div className="rounded bg-red-50 px-2 py-1 text-xs text-red-600">{stage.outbox.lastError}</div>
|
||||
) : null}
|
||||
|
||||
{canIntegrationManage && (externalCode || externalId) ? (
|
||||
{externalCode || externalId ? (
|
||||
<div className="rounded bg-zinc-50 px-2 py-1 text-xs text-zinc-600">
|
||||
ERP 编号:{externalCode ?? "-"}
|
||||
{externalId ? ` · ${externalId}` : ""}
|
||||
@@ -1324,7 +1348,7 @@ export function PurchaseRequestsPage() {
|
||||
) : (
|
||||
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">暂无 ERP 轨道信息</p>
|
||||
)}
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
<AiSuggestionCard targetType="purchase_request" targetId={detail.id} />
|
||||
|
||||
在新工单中引用
屏蔽一个用户