R7E-16D 审批动作二次确认

Refs: R7E-16D BIZ8-12B QAOPS8-17
这个提交包含在:
2026-05-15 21:20:49 +08:00
父节点 ff58b0d697
当前提交 96488d8052
共修改 20 个文件,包含 611 行新增和 67 行删除
+100 -43
查看文件
@@ -20,6 +20,7 @@ import {
} from "../api/workflow";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { MobileDetailHeader } from "../components/mobile/MobileDetailHeader";
@@ -33,6 +34,8 @@ import { MOBILE_APPROVAL_PERMISSIONS } from "../features/mobile/navigation";
import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay";
import { businessPrimaryLabel, compactBusinessReference, joinBusinessMeta } from "../lib/businessDisplay";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { cn } from "../lib/utils";
import { getWorkflowConfirmCopy, type WorkflowConfirmAction } from "../lib/workflowActionConfirm";
import {
cacheWorkflowTaskDetail,
enqueueWorkflowActionDraft,
@@ -390,12 +393,16 @@ export function MobileApprovalsPage() {
const [transferToApproverId, setTransferToApproverId] = useState("");
const [transferReason, setTransferReason] = useState("");
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
const [confirmAction, setConfirmAction] = useState<WorkflowConfirmAction | null>(null);
const [isOnline, setIsOnline] = useState(true);
const autoSyncRequestedRef = useRef(false);
const ownerEmployeeId = me?.id ?? null;
const canAccessRemoteApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]);
const canUseRemote = isOnline && Boolean(accessToken) && canAccessRemoteApprovals;
const confirmCopy = confirmAction
? getWorkflowConfirmCopy(confirmAction, { offlineDraft: !canUseRemote && confirmAction !== "return" })
: null;
const remoteTaskStatus = taskTab === "completed" ? "completed" : "pending";
const currentMobileSource = `${location.pathname}${location.search}`;
const employeeProgressStatus = progressStatusQuery(progressTab);
@@ -631,6 +638,26 @@ export function MobileApprovalsPage() {
queryFn: () => fetchWorkflowApprovers({ page: 1, pageSize: 100 }),
});
function requestMobileWorkflowAction(action: WorkflowConfirmAction) {
if (!canUseRemote && action === "return") {
setError("当前离线不支持退回,请联网后再处理。");
return;
}
if (action === "reject" && !comment.trim()) {
setError("驳回必须填写意见");
return;
}
if (action === "return" && !comment.trim()) {
setError("退回必须填写意见");
return;
}
setError("");
setConfirmAction(action);
}
async function refreshDraftQueueCache() {
if (!ownerEmployeeId) {
return [];
@@ -962,9 +989,18 @@ export function MobileApprovalsPage() {
const allVisibleSelected =
filteredTaskCards.length > 0 && filteredTaskCards.every((task) => selectedBatchTaskIds.includes(task.id));
const shouldShowDraftQueue = !isOnline || currentQueueItems.length > 0;
const showMobileApprovalActions = Boolean(isPushDetail && selectedTask && selectedTaskAllowsActions);
return (
<PageScaffold className="space-y-4">
<>
<PageScaffold
className={cn(
"space-y-4",
showMobileApprovalActions
? "mobile-fixed-action-page pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)]"
: "",
)}
>
{isPushDetail ? (
<MobileDetailHeader
title="审批详情"
@@ -1401,48 +1437,41 @@ export function MobileApprovalsPage() {
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
) : null}
<div className={canUseRemote ? "mobile-sticky-action-grid mobile-approval-action-grid" : "mobile-sticky-action-grid"}>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-approve"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("approve")}
onClick={() => {
if (canUseRemote) {
actionMutation.mutate("approve");
return;
}
void queueOfflineDraft("approve");
}}
type="button"
>
{canUseRemote ? "通过" : "保存通过草稿"}
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-reject"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("reject")}
onClick={() => {
if (canUseRemote) {
actionMutation.mutate("reject");
return;
}
void queueOfflineDraft("reject");
}}
type="button"
variant="secondary"
>
{canUseRemote ? "驳回" : "保存驳回草稿"}
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-return"
disabled={isActionSubmitting || !canUseRemote || !selectedTask.allowedActions.includes("return")}
onClick={() => actionMutation.mutate("return")}
type="button"
variant="secondary"
>
退回
</Button>
<div
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
data-testid="mobile-approvals-sticky-actions"
>
<div className="mobile-sticky-action-grid mobile-approval-action-grid">
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-approve"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("approve")}
onClick={() => requestMobileWorkflowAction("approve")}
type="button"
>
通过
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-reject"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("reject")}
onClick={() => requestMobileWorkflowAction("reject")}
type="button"
variant="secondary"
>
驳回
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-return"
disabled={isActionSubmitting || !canUseRemote || !selectedTask.allowedActions.includes("return")}
onClick={() => requestMobileWorkflowAction("return")}
type="button"
variant="secondary"
>
退回
</Button>
</div>
</div>
{canTransfer ? (
@@ -1587,5 +1616,33 @@ export function MobileApprovalsPage() {
</section>
) : null}
</PageScaffold>
<ConfirmDialog
open={Boolean(confirmCopy)}
onOpenChange={(open) => {
if (!open) {
setConfirmAction(null);
}
}}
title={confirmCopy?.title ?? ""}
description={confirmCopy?.description}
confirmLabel={confirmCopy?.confirmLabel}
variant={confirmCopy?.variant}
testId="workflow-action-confirm-dialog"
confirmTestId="workflow-action-confirm-submit"
cancelTestId="workflow-action-confirm-cancel"
onConfirm={async () => {
if (!confirmAction) {
return;
}
if (canUseRemote) {
await actionMutation.mutateAsync(confirmAction);
return;
}
if (confirmAction !== "return") {
await queueOfflineDraft(confirmAction);
}
}}
/>
</>
);
}