|
|
|
@@ -32,11 +32,12 @@ import { SectionPanel } from "../../components/patterns/SectionPanel";
|
|
|
|
|
import { Button } from "../../components/ui/button";
|
|
|
|
|
import { Label } from "../../components/ui/label";
|
|
|
|
|
import { Textarea } from "../../components/ui/textarea";
|
|
|
|
|
import { approvalRoleLabel, workflowActionLabel } from "../../lib/approvalDisplay";
|
|
|
|
|
import { workflowActionLabel } from "../../lib/approvalDisplay";
|
|
|
|
|
import { attachmentRequiresDownloadReason, type AttachmentGovernanceFields } from "../../lib/attachmentGovernance";
|
|
|
|
|
import { formatDisplayDateTime } from "../../lib/displayFormat";
|
|
|
|
|
import { buildReimbursementApprovalEvidence } from "../../lib/reimbursementApprovalProjection";
|
|
|
|
|
import { buildReimbursementWorkspaceModel, reimbursementAmountIntegrityMessage } from "../../lib/reimbursementWorkspace";
|
|
|
|
|
import { resolveReimbursementApprovalBlockReason } from "../../lib/reimbursementApprovalReadiness";
|
|
|
|
|
import { buildReimbursementWorkspaceModel } from "../../lib/reimbursementWorkspace";
|
|
|
|
|
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
|
|
|
|
|
import { isWecomWebView } from "../../lib/runtimeContext";
|
|
|
|
|
import { getWorkflowConfirmCopy, type WorkflowConfirmAction } from "../../lib/workflowActionConfirm";
|
|
|
|
@@ -56,20 +57,6 @@ function isAlreadyProcessedTaskError(error: unknown) {
|
|
|
|
|
return isApiError(error) && error.details?.task === "already_processed";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function workflowInstanceStatusLabel(status: string | undefined | null) {
|
|
|
|
|
const labels: Record<string, string> = {
|
|
|
|
|
pending: "审批中",
|
|
|
|
|
active: "审批中",
|
|
|
|
|
reviewing: "审批中",
|
|
|
|
|
approved: "已通过",
|
|
|
|
|
rejected: "已驳回",
|
|
|
|
|
terminated: "已终止",
|
|
|
|
|
cancelled: "已取消",
|
|
|
|
|
completed: "已完成",
|
|
|
|
|
};
|
|
|
|
|
return status ? labels[status] ?? status : "-";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function workflowTaskStatusLabel(status: string) {
|
|
|
|
|
const labels: Record<string, string> = {
|
|
|
|
|
pending: "待处理",
|
|
|
|
@@ -129,6 +116,14 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
&& model.amountSummary.integrity === "ready"
|
|
|
|
|
&& snapshot?.parseStatus === "ready",
|
|
|
|
|
);
|
|
|
|
|
const projectedDecisionBlockReason = resolveReimbursementApprovalBlockReason({
|
|
|
|
|
amountIntegrity: model?.amountSummary.integrity ?? null,
|
|
|
|
|
detailState: reimbursementDetailQuery.isError ? "error" : reimbursementDetailQuery.data ? "ready" : "loading",
|
|
|
|
|
online,
|
|
|
|
|
snapshotParseStatus: snapshot?.parseStatus,
|
|
|
|
|
});
|
|
|
|
|
const decisionBlockReason = projectedDecisionBlockReason
|
|
|
|
|
?? "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。";
|
|
|
|
|
const allowedActions = useMemo(() => new Set(detail.allowedActions), [detail.allowedActions]);
|
|
|
|
|
const canTransfer = allowedActions.has("transfer") && hasPermission(permissions, "expense.transfer");
|
|
|
|
|
const approverQuery = useQuery({
|
|
|
|
@@ -175,11 +170,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
const actionMutation = useMutation({
|
|
|
|
|
mutationFn: async (action: "approve" | "reject" | "return" | "transfer") => {
|
|
|
|
|
if (!decisionReady) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
model
|
|
|
|
|
? reimbursementAmountIntegrityMessage(model.amountSummary.integrity) ?? "完整报销内容或提交时检查尚未就绪,暂时不能处理"
|
|
|
|
|
: "完整报销内容或提交时检查尚未就绪,暂时不能处理",
|
|
|
|
|
);
|
|
|
|
|
throw new Error(decisionBlockReason);
|
|
|
|
|
}
|
|
|
|
|
if (action === "approve") return approveWorkflowTask(taskId);
|
|
|
|
|
if (action === "reject") return rejectWorkflowTask(taskId, rejectComment.trim());
|
|
|
|
@@ -209,11 +200,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
|
|
|
|
|
function requestAction(action: WorkflowConfirmAction) {
|
|
|
|
|
if (!decisionReady) {
|
|
|
|
|
setActionError(!online
|
|
|
|
|
? "报销审批需联网核对完整报销内容后才能处理。"
|
|
|
|
|
: model
|
|
|
|
|
? reimbursementAmountIntegrityMessage(model.amountSummary.integrity) ?? "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。"
|
|
|
|
|
: "完整报销内容或提交时检查尚未就绪,请重试加载后再决定。");
|
|
|
|
|
setActionError(decisionBlockReason);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const comment = action === "reject" ? rejectComment : action === "return" ? returnComment : "";
|
|
|
|
@@ -245,13 +232,6 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!model) {
|
|
|
|
|
const message = !online
|
|
|
|
|
? "报销审批需联网核对完整报销内容;当前不能通过、退回、驳回或转审。"
|
|
|
|
|
: snapshot?.parseStatus === "invalid"
|
|
|
|
|
? "提交时检查记录无法解析,暂时不能审批;请联系系统管理员核对。"
|
|
|
|
|
: reimbursementDetailQuery.isError
|
|
|
|
|
? resolveUserErrorMessage(reimbursementDetailQuery.error, "完整报销内容加载失败,暂时不能审批。")
|
|
|
|
|
: "正在加载完整报销内容和票据;加载完成前不能作出审批决定。";
|
|
|
|
|
const disabledActions = (
|
|
|
|
|
<div className="grid grid-cols-2 gap-2" data-testid="reimbursement-approval-fail-closed-actions">
|
|
|
|
|
<Button data-testid={layout === "mobile" ? "mobile-approvals-approve" : "workflow-task-approve"} disabled type="button">通过</Button>
|
|
|
|
@@ -262,7 +242,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<div className={layout === "mobile" ? "mobile-card space-y-3" : "card-std mx-auto max-w-3xl space-y-3 p-5"} role="alert">
|
|
|
|
|
<p className="text-sm text-zinc-700">{message}</p>
|
|
|
|
|
<p className="text-sm text-zinc-700">{decisionBlockReason}</p>
|
|
|
|
|
{disabledActions}
|
|
|
|
|
{online && reimbursementDetailQuery.isError ? <Button onClick={() => reimbursementDetailQuery.refetch()} type="button" variant="secondary">重试加载</Button> : null}
|
|
|
|
|
<Button asChild type="button" variant="secondary"><Link to={layout === "mobile" ? "/mobile/approvals" : "/workflow/tasks"}>返回审批列表</Link></Button>
|
|
|
|
@@ -272,25 +252,26 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
|
|
|
|
|
const confirmCopy = confirmAction ? getWorkflowConfirmCopy(confirmAction) : null;
|
|
|
|
|
const actionDisabled = actionMutation.isPending || !decisionReady;
|
|
|
|
|
const issueAnchors = model.issueAnchors.length ? (
|
|
|
|
|
const visibleIssueAnchors = model.issueAnchors.filter((issue) => issue.label !== decisionBlockReason);
|
|
|
|
|
const issueAnchors = visibleIssueAnchors.length ? (
|
|
|
|
|
<div className="space-y-2" data-testid="reimbursement-workspace-issue-anchors">
|
|
|
|
|
<p className="text-sm font-medium text-zinc-900">需要关注</p>
|
|
|
|
|
{model.issueAnchors.map((issue) => (
|
|
|
|
|
{visibleIssueAnchors.map((issue) => (
|
|
|
|
|
<a className="block text-sm text-zinc-700 underline-offset-4 hover:underline" href={`#${issue.id}`} key={`${issue.id}-${issue.label}`}>{issue.label}</a>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : null;
|
|
|
|
|
const statusSummary = (
|
|
|
|
|
<>
|
|
|
|
|
<div>节点:{detail.task.stepName}</div>
|
|
|
|
|
<div>状态:{workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus)}</div>
|
|
|
|
|
<div>审批角色:{approvalRoleLabel(detail.task.assigneeRole)}</div>
|
|
|
|
|
<div>{decisionReady ? "完整内容已就绪" : "内容未就绪,审批动作已停用"}</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
const decisionBlockNotice = !decisionReady && decisionBlockReason ? (
|
|
|
|
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" data-testid="reimbursement-approval-block-reason" role="alert">
|
|
|
|
|
{decisionBlockReason}
|
|
|
|
|
</p>
|
|
|
|
|
) : null;
|
|
|
|
|
const actionPanelBeforeActions = issueAnchors || decisionBlockNotice ? (
|
|
|
|
|
<div className="space-y-3">{issueAnchors}{decisionBlockNotice}</div>
|
|
|
|
|
) : null;
|
|
|
|
|
const offlineNotice = !online ? (
|
|
|
|
|
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900" role="alert">
|
|
|
|
|
报销审批需联网核对完整报销内容;当前不能通过、退回、驳回或转审。
|
|
|
|
|
{decisionBlockReason}
|
|
|
|
|
</p>
|
|
|
|
|
) : null;
|
|
|
|
|
const commentEditor = (
|
|
|
|
@@ -417,6 +398,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
const actionPanel = (
|
|
|
|
|
<BusinessDetailActionPanel
|
|
|
|
|
title="审批决定"
|
|
|
|
|
beforeActions={actionPanelBeforeActions}
|
|
|
|
|
primaryAction={allowedActions.has("approve") ? <Button data-testid="workflow-task-approve" disabled={actionDisabled} onClick={() => requestAction("approve")} type="button">通过</Button> : null}
|
|
|
|
|
secondaryActions={(
|
|
|
|
|
<>
|
|
|
|
@@ -425,10 +407,10 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
moreActions={canTransfer ? <Button className="w-full" data-testid="workflow-task-transfer-menu-item" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "transfer" ? null : "transfer")} type="button" variant="ghost">转审</Button> : null}
|
|
|
|
|
summary={statusSummary}
|
|
|
|
|
summary={null}
|
|
|
|
|
testId="workflow-task-action-panel"
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-3">{issueAnchors}{commentEditor}</div>
|
|
|
|
|
{actionError || activeSecondaryAction ? <div className="space-y-3">{commentEditor}</div> : null}
|
|
|
|
|
</BusinessDetailActionPanel>
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|