import type { ReactNode } from "react"; import { useMemo, useState } from "react"; import { Link, Navigate, useNavigate, useParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; import { approveWorkflowTask, fetchWorkflowApprovers, fetchWorkflowTaskDetail, rejectWorkflowTask, returnWorkflowTask, transferWorkflowTask, } from "../api/workflow"; import { toast } from "sonner"; import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { buildMobileApprovalsPath, shouldPreferMobileApprovalExperience } from "../pwa"; import { CardSkeleton } from "../components/common/CardSkeleton"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { StatusBadge } from "../components/common/StatusBadge"; import { BusinessDetailActionPanel, BusinessDetailAside, BusinessDetailMain, BusinessDetailWorkspace, } from "../components/patterns/BusinessDetailTemplate"; import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; import { StatStrip } from "../components/patterns/ListWorkspace"; import { MetricCard } from "../components/patterns/MetricCard"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { Button } from "../components/ui/button"; import { Textarea } from "../components/ui/textarea"; import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay"; import { formatDisplayDateTime } from "../lib/displayFormat"; import { getWorkflowConfirmCopy, type WorkflowConfirmAction } from "../lib/workflowActionConfirm"; function entityTitle(entityType: string | undefined) { if (entityType === "leave") { return "请假申请"; } if (entityType === "reimbursement") { return "报销单"; } if (entityType === "attendance_correction") { return "考勤修正"; } if (entityType === "purchase_request") { return "采购申请"; } if (entityType === "travel_request") { return "差旅申请"; } return entityType || "业务单据"; } function correctionKindLabel(kind: string | undefined | null) { if (kind === "missed_punch") return "补卡"; if (kind === "out") return "外出"; if (kind === "overtime") return "加班"; return "考勤修正"; } function workflowInstanceStatusLabel(status: string | undefined | null) { if (!status) { return "-"; } const labels: Record = { pending: "审批中", active: "审批中", approved: "已通过", rejected: "已驳回", terminated: "已终止", cancelled: "已取消", completed: "已完成", }; return labels[status] ?? status; } function workflowActionLabel(action: string | undefined | null) { if (!action) { return "-"; } const labels: Record = { submit: "提交", approve: "通过", reject: "驳回", return: "退回", transfer: "转审", withdraw: "撤回", remind: "催办", overdue_remind: "超时提醒", cancel: "取消", skip: "跳过", }; return labels[action] ?? action; } function DetailField({ label, value }: { label: string; value: ReactNode }) { return (
{label}
{value || "-"}
); } function isAlreadyProcessedTaskError(error: unknown) { return isApiError(error) && error.details?.task === "already_processed"; } type SecondaryWorkflowAction = "reject" | "return" | "transfer"; export function WorkflowTaskDetailPage() { const { taskId = "" } = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { permissions } = useAuth(); const [rejectComment, setRejectComment] = useState("请补充必要说明后重提"); const [returnComment, setReturnComment] = useState("请补充必要信息并修正后重新提交"); const [transferReason, setTransferReason] = useState("当前事项需转交对应审批人处理,请尽快处理。"); const [selectedApprover, setSelectedApprover] = useState(""); const [actionError, setActionError] = useState(""); const [activeSecondaryAction, setActiveSecondaryAction] = useState(null); const [confirmAction, setConfirmAction] = useState(null); const [transferConfirmOpen, setTransferConfirmOpen] = useState(false); const detailQuery = useQuery({ enabled: Boolean(taskId), queryKey: ["workflow", "task", taskId], queryFn: () => fetchWorkflowTaskDetail(taskId), }); const approverQuery = useQuery({ enabled: hasPermission(permissions, "expense.transfer"), queryKey: ["workflow", "approvers"], queryFn: () => fetchWorkflowApprovers({ page: 1, pageSize: 50 }), }); const allowedActions = useMemo(() => { return new Set(detailQuery.data?.allowedActions ?? []); }, [detailQuery.data?.allowedActions]); const mutation = useMutation({ mutationFn: async (action: "approve" | "reject" | "return" | "transfer") => { if (!taskId) { throw new Error("missing taskId"); } 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()); }, onSuccess: async (_data, action) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }), queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }), ]); setActionError(""); setActiveSecondaryAction(null); setTransferConfirmOpen(false); const messages: Record = { approve: "已通过", reject: "已驳回", return: "已退回", transfer: "已转审" }; toast.success(messages[action] ?? "操作成功"); }, onError: (error) => { if (isAlreadyProcessedTaskError(error)) { void Promise.all([ queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }), queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }), ]); } let msg: string; if (error instanceof Error && !isApiError(error)) { msg = error.message; } else if (isApiError(error)) { msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } else { msg = "动作执行失败,请稍后重试"; } setActionError(msg); toast.error(msg); }, }); const confirmCopy = confirmAction ? getWorkflowConfirmCopy(confirmAction) : null; function requestWorkflowAction(action: WorkflowConfirmAction) { if (action === "reject" && rejectComment.trim().length < 1) { setActionError("驳回必须填写意见"); return; } if (action === "return" && returnComment.trim().length < 1) { setActionError("退回必须填写意见"); return; } setActionError(""); setConfirmAction(action); } function requestWorkflowTransfer() { if (!selectedApprover) { setActionError("请选择转审目标"); return; } if (transferReason.trim().length < 10) { setActionError("转审理由至少 10 个字"); return; } setActionError(""); setTransferConfirmOpen(true); } if (!taskId) { return (

缺少 taskId 参数。

); } if (shouldPreferMobileApprovalExperience()) { return ; } if (detailQuery.isLoading) { return ; } if (detailQuery.isError || !detailQuery.data) { const message = isApiError(detailQuery.error) ? `${detailQuery.error.code}: ${mapApiErrorMessage( detailQuery.error.code, detailQuery.error.details, detailQuery.error.message, )}` : "加载失败,请返回列表重试"; return (

{message}

); } const detail = detailQuery.data; const canTransfer = allowedActions.has("transfer"); const allowedActionLabels = detail.allowedActions.map(workflowActionLabel).join(" / "); const businessStatusLabel = workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus); return ( <> 业务状态:{businessStatusLabel} 类型:{entityTitle(detail.workflowInstance?.entityType)} 节点:{detail.task.stepName} )} actions={( )} />
{detail.task.stepName}} /> {entityTitle(detail.workflowInstance?.entityType)}} /> {allowedActionLabels || "-"}} /> {formatDisplayDateTime(detail.task.createdAt)}} />
{detail.reimbursement ? ( <> ) : null} {detail.leaveRequest ? ( <> {detail.leaveRequest.startDate ?? "-"} {detail.leaveRequest.startSession === "am" ? "上午" : "下午"} {" → "} {detail.leaveRequest.endDate ?? "-"} {detail.leaveRequest.endSession === "am" ? "上午" : "下午"} } /> ) : null} {detail.attendanceCorrection ? ( <> ) : null} {detail.purchaseRequest ? ( <> ) : null} {detail.travelRequest ? ( <> `${item.fromCity}→${item.toCity}(${item.segmentRole})`) .join(";") : "-" } /> ) : null}
{detail.reimbursement ? ( ) : null}
{detail.workflowInstance?.entityType === "reimbursement" && detail.workflowInstance.contextData.standardViolations.length ? (
{detail.workflowInstance.contextData.standardViolations.map((item) => (
{item.expenseTypeName}
当前 ¥{item.amount} / 标准 ¥{item.maxAmount}/{item.perUnit}
超出 ¥{item.exceededAmount} · {item.standardName}
管控模式:{item.controlMode === "block" ? "阻断" : "预警"}
))}
) : null}
{detail.history.length ? ( detail.history.map((item) => (
{workflowActionLabel(item.action)} · {item.actorName || item.actorId}
{item.comment || "-"}
{item.createdAt || "-"}
)) ) : (

暂无历史记录

)}
{canTransfer ? (
更多
) : null} )} summary={( <>
节点:{detail.task.stepName}
实例状态:{workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus)}
当前审批角色:{detail.task.assigneeRole ?? "-"}
创建时间:{formatDisplayDateTime(detail.task.createdAt)}
完成时间:{formatDisplayDateTime(detail.task.completedAt)}
发起时间:{formatDisplayDateTime(detail.workflowInstance?.startedAt)}
)} testId="workflow-task-action-panel" > {actionError ? (

{actionError}

) : null} {activeSecondaryAction === "reject" ? (

驳回意见