收敛报销退回动作与审批布局 [09-04-approval-return-reject-actions]
这个提交包含在:
@@ -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"
|
||||
|
||||
在新工单中引用
屏蔽一个用户