实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)
这个提交包含在:
@@ -35,6 +35,8 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { AttachmentGovernanceBadges } from "../components/common/AttachmentGovernanceBadges";
|
||||
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
@@ -62,6 +64,7 @@ import {
|
||||
} from "../lib/invoiceStatus";
|
||||
import { useInvoiceOcrTracker } from "../hooks/useInvoiceOcrTracker";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance";
|
||||
import { formatDisplayDate } from "../lib/displayFormat";
|
||||
import { TEST_IDS } from "../lib/testIds";
|
||||
import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles";
|
||||
@@ -726,8 +729,11 @@ export function ReimbursementDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { requestDownloadReason, downloadReasonDialog } = useAttachmentDownloadReasonDialog();
|
||||
|
||||
const openAttachmentMutation = useMutation({
|
||||
mutationFn: (attachmentId: string) => downloadReimbursementAttachment(reimbursementId, attachmentId),
|
||||
mutationFn: ({ attachmentId, reason }: { attachmentId: string; reason?: string }) =>
|
||||
downloadReimbursementAttachment(reimbursementId, attachmentId, reason),
|
||||
onSuccess: (result) => {
|
||||
const url = URL.createObjectURL(result.blob);
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
@@ -738,6 +744,14 @@ export function ReimbursementDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
async function handleOpenAttachment(attachment: { id: string; originalName?: string } & AttachmentGovernanceFields) {
|
||||
const reason = await requestDownloadReason(attachment);
|
||||
if (reason === null) {
|
||||
return;
|
||||
}
|
||||
openAttachmentMutation.mutate({ attachmentId: attachment.id, reason });
|
||||
}
|
||||
|
||||
if (!reimbursementId) {
|
||||
return (
|
||||
<div className="card-std p-5">
|
||||
@@ -959,6 +973,7 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
return (
|
||||
<PageScaffold>
|
||||
{downloadReasonDialog}
|
||||
<EntityOverviewHero
|
||||
title={`报销详情 ${displayCode}`}
|
||||
subtitle="状态、票据、审批和付款集中处理。"
|
||||
@@ -1197,6 +1212,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="text-xs text-zinc-500">
|
||||
{Math.ceil(attachment.fileSize / 1024)} KB · {formatDateTime(attachment.createdAt)}
|
||||
</div>
|
||||
<AttachmentGovernanceBadges attachment={attachment} className="mt-2" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -1204,7 +1220,7 @@ export function ReimbursementDetailPage() {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={openAttachmentMutation.isPending}
|
||||
onClick={() => openAttachmentMutation.mutate(attachment.id)}
|
||||
onClick={() => void handleOpenAttachment(attachment)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
|
||||
在新工单中引用
屏蔽一个用户