收敛报销退回动作与审批布局 [09-04-approval-return-reject-actions]

这个提交包含在:
2026-09-04 10:31:32 +08:00
父节点 59df0594bb
当前提交 8551601818
共修改 17 个文件,包含 520 行新增和 56 行删除
@@ -6,7 +6,6 @@ import type { WorkflowTaskDetail } from "../../api/workflow";
import {
approveWorkflowTask,
fetchWorkflowApprovers,
rejectWorkflowTask,
returnWorkflowTask,
transferWorkflowTask,
} from "../../api/workflow";
@@ -40,10 +39,11 @@ import { resolveReimbursementApprovalBlockReason } from "../../lib/reimbursement
import { buildReimbursementWorkspaceModel } from "../../lib/reimbursementWorkspace";
import { resolveErrorMessage as resolveUserErrorMessage } from "../../lib/resolveErrorMessage";
import { isWecomWebView } from "../../lib/runtimeContext";
import { getWorkflowConfirmCopy, type WorkflowConfirmAction } from "../../lib/workflowActionConfirm";
import { getWorkflowConfirmCopy } from "../../lib/workflowActionConfirm";
import { openFileInBrowser, previewFileInWecom } from "../../lib/wecomFilePreview";
type SecondaryWorkflowAction = "reject" | "return" | "transfer";
type SecondaryWorkflowAction = "return" | "transfer";
type ReimbursementConfirmAction = "approve" | "return";
type Props = {
detail: WorkflowTaskDetail;
@@ -84,13 +84,12 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
const { permissions } = useAuth();
const backTarget = resolveApprovalBackTarget((location.state as { from?: unknown } | null)?.from, layout);
const reimbursementId = detail.reimbursement?.id ?? "";
const [rejectComment, setRejectComment] = useState("");
const [returnComment, setReturnComment] = useState("");
const [transferReason, setTransferReason] = useState("");
const [selectedApprover, setSelectedApprover] = useState("");
const [actionError, setActionError] = useState("");
const [activeSecondaryAction, setActiveSecondaryAction] = useState<SecondaryWorkflowAction | null>(null);
const [confirmAction, setConfirmAction] = useState<WorkflowConfirmAction | null>(null);
const [confirmAction, setConfirmAction] = useState<ReimbursementConfirmAction | null>(null);
const [transferConfirmOpen, setTransferConfirmOpen] = useState(false);
const reimbursementDetailQuery = useQuery({
@@ -168,12 +167,11 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
});
const actionMutation = useMutation({
mutationFn: async (action: "approve" | "reject" | "return" | "transfer") => {
mutationFn: async (action: "approve" | "return" | "transfer") => {
if (!decisionReady) {
throw new Error(decisionBlockReason);
}
if (action === "approve") return approveWorkflowTask(taskId);
if (action === "reject") return rejectWorkflowTask(taskId, rejectComment.trim());
if (action === "return") return returnWorkflowTask(taskId, returnComment.trim());
if (!selectedApprover) throw new Error("请选择转审目标");
return transferWorkflowTask(taskId, selectedApprover, transferReason.trim());
@@ -188,7 +186,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
setActiveSecondaryAction(null);
setConfirmAction(null);
setTransferConfirmOpen(false);
toast.success({ approve: "已通过", reject: "已驳回", return: "已退回", transfer: "已转审" }[action]);
toast.success({ approve: "已通过", return: "已退回", transfer: "已转审" }[action]);
},
onError: (error) => {
if (isAlreadyProcessedTaskError(error)) void refetchTask();
@@ -198,14 +196,14 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
},
});
function requestAction(action: WorkflowConfirmAction) {
function requestAction(action: ReimbursementConfirmAction) {
if (!decisionReady) {
setActionError(decisionBlockReason);
return;
}
const comment = action === "reject" ? rejectComment : action === "return" ? returnComment : "";
if ((action === "reject" || action === "return") && !comment.trim()) {
setActionError(`${action === "reject" ? "驳回" : "退回"}必须填写意见`);
const comment = action === "return" ? returnComment : "";
if (action === "return" && !comment.trim()) {
setActionError("退回修改必须填写意见");
return;
}
setActionError("");
@@ -235,8 +233,7 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
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>
<Button data-testid={layout === "mobile" ? "mobile-approvals-return" : "workflow-task-return-toggle"} disabled type="button" variant="secondary">退回</Button>
<Button data-testid={layout === "mobile" ? "mobile-approvals-reject" : "workflow-task-reject-toggle"} disabled type="button" variant="secondary">驳回</Button>
<Button data-testid={layout === "mobile" ? "mobile-approvals-return" : "workflow-task-return-toggle"} disabled type="button" variant="secondary">退回修改</Button>
<Button data-testid={layout === "mobile" ? "mobile-approvals-transfer" : "workflow-task-transfer-menu-item"} disabled type="button" variant="secondary">转审</Button>
</div>
);
@@ -250,7 +247,10 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
);
}
const confirmCopy = confirmAction ? getWorkflowConfirmCopy(confirmAction) : null;
const baseConfirmCopy = confirmAction ? getWorkflowConfirmCopy(confirmAction) : null;
const confirmCopy = confirmAction === "return" && baseConfirmCopy
? { ...baseConfirmCopy, title: "确认退回修改?", confirmLabel: "确认退回修改" }
: baseConfirmCopy;
const actionDisabled = actionMutation.isPending || !decisionReady;
const visibleIssueAnchors = model.issueAnchors.filter((issue) => issue.label !== decisionBlockReason);
const issueAnchors = visibleIssueAnchors.length ? (
@@ -279,19 +279,11 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
{actionError ? <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" role="alert">{actionError}</p> : null}
{activeSecondaryAction === "return" ? (
<div className="space-y-2">
<Label htmlFor={`${layout}-reimbursement-return-comment`}>退回意见</Label>
<Label htmlFor={`${layout}-reimbursement-return-comment`}>退回修改意见</Label>
<p className="text-xs text-zinc-500">退回后原单可修改并重新提交,单号和审批历史会保留。</p>
<Textarea data-testid={layout === "desktop" ? "workflow-task-return-comment" : "mobile-approvals-comment"} id={`${layout}-reimbursement-return-comment`} value={returnComment} onChange={(event) => setReturnComment(event.target.value)} />
<ReimbursementReturnReferencePicker evidence={model.evidence} onReference={appendReturnReference} />
<Button className="min-h-11 w-full" data-testid={layout === "desktop" ? "workflow-task-return" : "mobile-approvals-return-submit"} disabled={actionDisabled || !returnComment.trim()} onClick={() => requestAction("return")} type="button" variant="secondary">确认退回</Button>
</div>
) : null}
{activeSecondaryAction === "reject" ? (
<div className="space-y-2">
<Label htmlFor={`${layout}-reimbursement-reject-comment`}>驳回意见</Label>
<p className="text-xs text-zinc-500">驳回会终止本次流程,申请人通常需要重新发起。</p>
<Textarea data-testid={layout === "desktop" ? "workflow-task-reject-comment" : "mobile-approvals-comment"} id={`${layout}-reimbursement-reject-comment`} value={rejectComment} onChange={(event) => setRejectComment(event.target.value)} />
<Button className="min-h-11 w-full" data-testid={layout === "desktop" ? "workflow-task-reject" : "mobile-approvals-reject-submit"} disabled={actionDisabled || !rejectComment.trim()} onClick={() => requestAction("reject")} type="button" variant="secondary">确认驳回</Button>
<Button className="min-h-11 w-full" data-testid={layout === "desktop" ? "workflow-task-return" : "mobile-approvals-return-submit"} disabled={actionDisabled || !returnComment.trim()} onClick={() => requestAction("return")} type="button" variant="secondary">确认退回修改</Button>
</div>
) : null}
{activeSecondaryAction === "transfer" ? (
@@ -365,13 +357,10 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
{offlineNotice}
{issueAnchors}
{commentEditor}
<div className="grid grid-cols-2 gap-2">
{allowedActions.has("return") || !online ? <Button className="min-h-11" data-testid="mobile-approvals-return" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")} type="button" variant="secondary">退回</Button> : null}
{allowedActions.has("reject") || !online ? <Button className="min-h-11" data-testid="mobile-approvals-reject" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "reject" ? null : "reject")} type="button" variant="secondary">驳回</Button> : null}
{canTransfer || !online ? <Button className="min-h-11" data-testid="mobile-approvals-transfer" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "transfer" ? null : "transfer")} type="button" variant="secondary">转审</Button> : null}
</div>
{canTransfer || !online ? <Button className="min-h-11 w-full" data-testid="mobile-approvals-transfer" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "transfer" ? null : "transfer")} type="button" variant="ghost">转审</Button> : null}
</section>
<MobileStickyActionBar testId="mobile-approvals-sticky-actions">
<MobileStickyActionBar contentClassName="grid-cols-2" testId="mobile-approvals-sticky-actions">
{allowedActions.has("return") || !online ? <Button className="min-h-12 w-full" data-testid="mobile-approvals-return" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")} type="button" variant="secondary">退回修改</Button> : null}
<Button className="min-h-12 w-full" data-testid="mobile-approvals-approve" disabled={actionDisabled || !allowedActions.has("approve")} onClick={() => requestAction("approve")} type="button">通过</Button>
</MobileStickyActionBar>
</>
@@ -399,13 +388,13 @@ export function ReimbursementApprovalAdapter({ detail, layout, refetchTask, task
<BusinessDetailActionPanel
title="审批决定"
beforeActions={actionPanelBeforeActions}
primaryAction={allowedActions.has("approve") ? <Button data-testid="workflow-task-approve" disabled={actionDisabled} onClick={() => requestAction("approve")} type="button">通过</Button> : null}
secondaryActions={(
<>
{allowedActions.has("return") ? <Button data-testid="workflow-task-return-toggle" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")} type="button" variant="secondary">退回</Button> : null}
{allowedActions.has("reject") ? <Button data-testid="workflow-task-reject-toggle" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "reject" ? null : "reject")} type="button" variant="secondary">驳回</Button> : null}
</>
)}
primaryAction={allowedActions.has("approve") || allowedActions.has("return") ? (
<div className="grid grid-cols-2 gap-2" data-testid="reimbursement-approval-primary-actions">
{allowedActions.has("approve") ? <Button className="min-h-11 w-full" data-testid="workflow-task-approve" disabled={actionDisabled} onClick={() => requestAction("approve")} type="button">通过</Button> : null}
{allowedActions.has("return") ? <Button className="min-h-11 w-full" data-testid="workflow-task-return-toggle" disabled={actionDisabled} onClick={() => setActiveSecondaryAction(activeSecondaryAction === "return" ? null : "return")} type="button" variant="secondary">退回修改</Button> : null}
</div>
) : null}
secondaryActions={null}
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={null}
testId="workflow-task-action-panel"