feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面
关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
@@ -12,6 +12,7 @@ import {
|
||||
fetchPurchaseRequestErpTrack,
|
||||
fetchPurchaseRequests,
|
||||
issuePurchaseAttachmentSignedUrl,
|
||||
precheckPurchaseRequest,
|
||||
requeuePurchaseRequestErpSync,
|
||||
submitPurchaseRequest,
|
||||
updatePurchaseRequest,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
@@ -40,6 +42,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 { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
@@ -211,6 +214,7 @@ export function PurchaseRequestsPage() {
|
||||
const [costCenterFilter, setCostCenterFilter] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedId, setSelectedId] = useState("");
|
||||
const [hydratedDetailId, setHydratedDetailId] = useState("");
|
||||
const [formError, setFormError] = useState("");
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
@@ -296,7 +300,7 @@ export function PurchaseRequestsPage() {
|
||||
}, [detailQuery.isError, detailQuery.isLoading, listQuery.data?.items, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailQuery.data) {
|
||||
if (!detailQuery.data || detailQuery.data.id !== selectedId || hydratedDetailId === detailQuery.data.id) {
|
||||
return;
|
||||
}
|
||||
setTitle(detailQuery.data.title || "");
|
||||
@@ -320,7 +324,8 @@ export function PurchaseRequestsPage() {
|
||||
setArchivePoNo(detailQuery.data.archiveOrder?.poNo || "");
|
||||
setArchiveContractNo(detailQuery.data.archiveOrder?.contractNo || "");
|
||||
setFormError("");
|
||||
}, [detailQuery.data]);
|
||||
setHydratedDetailId(detailQuery.data.id);
|
||||
}, [detailQuery.data, hydratedDetailId, selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!archiveOrderQuery.data) {
|
||||
@@ -355,6 +360,7 @@ export function PurchaseRequestsPage() {
|
||||
mutationFn: () => createPurchaseRequest(),
|
||||
onSuccess: async (data) => {
|
||||
setSelectedId(data.purchaseRequest.id);
|
||||
setHydratedDetailId("");
|
||||
await queryClient.invalidateQueries({ queryKey: ["purchase-requests", "list"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail"] });
|
||||
toast.success("采购草稿已创建");
|
||||
@@ -444,21 +450,22 @@ export function PurchaseRequestsPage() {
|
||||
if (!selectedId) {
|
||||
throw new Error("请先选择采购单");
|
||||
}
|
||||
validateBeforeSubmit();
|
||||
await updatePurchaseRequest(selectedId, buildPayload());
|
||||
return submitPurchaseRequest(selectedId);
|
||||
},
|
||||
onSuccess: async (data) => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["purchase-requests"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["purchase-requests", "erp-track", selectedId] }),
|
||||
]);
|
||||
setFormError("");
|
||||
toast.success(
|
||||
data.warnings.includes("missing_budget_pool")
|
||||
? "采购申请已提交(未匹配预算池)"
|
||||
: "采购申请已提交",
|
||||
data.precheckDecision === "manual_review"
|
||||
? `采购申请已提交,已生成 ${data.openedRiskFlagCount} 条待复核风险`
|
||||
: data.warnings.includes("missing_budget_pool")
|
||||
? "采购申请已提交(未匹配预算池)"
|
||||
: "采购申请已提交",
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -606,6 +613,36 @@ export function PurchaseRequestsPage() {
|
||||
|
||||
const detail = detailQuery.data;
|
||||
const archiveOrder = archiveOrderQuery.data?.archiveOrder ?? detail?.archiveOrder ?? null;
|
||||
|
||||
const handleSubmitAction = async () => {
|
||||
if (!selectedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setFormError("");
|
||||
validateBeforeSubmit();
|
||||
await updatePurchaseRequest(selectedId, buildPayload());
|
||||
const precheck = await precheckPurchaseRequest(selectedId);
|
||||
const message = buildPrecheckDialogMessage(precheck);
|
||||
|
||||
if (precheck.decision === "block") {
|
||||
setFormError(message);
|
||||
toast.error("提交前校验未通过");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitMutation.mutate();
|
||||
} catch (error) {
|
||||
const message = resolveError(error);
|
||||
setFormError(message);
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
const erpTrack = erpTrackQuery.data ?? null;
|
||||
const canRetryErp = Boolean(canIntegrationManage && erpTrack?.permissions.canRetry);
|
||||
const showArchiveSection = Boolean(detail && (detail.status === "approved" || archiveOrder));
|
||||
@@ -767,7 +804,10 @@ export function PurchaseRequestsPage() {
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={item.id === selectedId ? "bg-zinc-50" : undefined}
|
||||
onClick={() => setSelectedId(item.id)}
|
||||
onClick={() => {
|
||||
setSelectedId(item.id);
|
||||
setHydratedDetailId("");
|
||||
}}
|
||||
>
|
||||
<TableCell className="cursor-pointer">
|
||||
<div className="font-medium text-zinc-900">{item.requestNo || item.title || item.id}</div>
|
||||
@@ -835,7 +875,7 @@ export function PurchaseRequestsPage() {
|
||||
data-testid="purchase-submit"
|
||||
type="button"
|
||||
disabled={!canSubmit || submitMutation.isPending}
|
||||
onClick={() => submitMutation.mutate()}
|
||||
onClick={() => void handleSubmitAction()}
|
||||
>
|
||||
{submitMutation.isPending ? "提交中..." : "提交审批"}
|
||||
</Button>
|
||||
@@ -851,6 +891,8 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">标题</label>
|
||||
|
||||
在新工单中引用
屏蔽一个用户