AP6 增加审批催办与效率摘要

这个提交包含在:
2026-05-12 10:41:47 +08:00
父节点 f99fbce3d8
当前提交 72e38db4e5
共修改 23 个文件,包含 1362 行新增和 12 行删除
@@ -24,6 +24,7 @@ import {
type ReimbursementItem,
type ReimbursementStatus,
} from "../api/reimbursements";
import { remindWorkflowTask } from "../api/workflow";
import {
bindInvoice,
fetchBindableInvoices,
@@ -109,6 +110,20 @@ function invoiceStatusLabel(status: string) {
return INVOICE_STATUS_LABEL[status] ?? "未知状态";
}
function workflowActionLabel(action: string) {
const labels: Record<string, string> = {
start: "提交",
approve: "通过",
reject: "驳回",
return: "退回",
transfer: "转审",
withdraw: "撤回",
remind: "催办",
overdue_remind: "超时提醒",
};
return labels[action] ?? action;
}
function resolveErpSyncStatus(
exportState:
| {
@@ -298,6 +313,23 @@ export function ReimbursementDetailPage() {
},
});
const remindMutation = useMutation({
mutationFn: (taskId: string) => remindWorkflowTask(taskId),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] }),
queryClient.invalidateQueries({ queryKey: ["notifications"] }),
queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }),
queryClient.invalidateQueries({ queryKey: ["workflow", "tasks", "summary"] }),
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
]);
toast.success("已催办当前审批人");
},
onError: (error) => {
toast.error(buildActionErrorMessage(error, "催办失败,请稍后重试"));
},
});
const itemSaveMutation = useMutation({
mutationFn: async (data: ItemEditorSaveData) => {
if (!reimbursementId) {
@@ -766,6 +798,13 @@ export function ReimbursementDetailPage() {
const canPay = hasPermission(permissions, "expense.finance.pay");
const canEditItems = ["draft", "rejected"].includes(detail.status);
const isReimbursementParticipant = Boolean(me?.id) && [detail.ownerId, detail.submitterId].includes(me?.id ?? "");
const pendingTaskId = detail.pendingWorkflow?.task?.id ?? null;
const canRemindPendingWorkflow = Boolean(
detail.status === "reviewing" &&
pendingTaskId &&
isReimbursementParticipant &&
detail.pendingWorkflow?.task?.assignee_id !== me?.id,
);
const canOpenInvoiceBindDialog =
canEditItems &&
isReimbursementParticipant &&
@@ -944,6 +983,17 @@ export function ReimbursementDetailPage() {
<Button asChild variant="secondary" data-testid="reimbursement-back">
<Link to="/reimbursements">返回列表</Link>
</Button>
{canRemindPendingWorkflow && pendingTaskId ? (
<Button
data-testid="reimbursement-remind-workflow"
disabled={remindMutation.isPending}
onClick={() => remindMutation.mutate(pendingTaskId)}
type="button"
variant="secondary"
>
{remindMutation.isPending ? "催办中..." : "催办审批"}
</Button>
) : null}
{canDeleteReimbursement ? (
<Button
data-testid="reimbursement-delete-draft"
@@ -1540,6 +1590,27 @@ export function ReimbursementDetailPage() {
</DetailSideMeta>
) : null}
<DetailSideMeta title="审批记录">
{detail.approvalHistory.length ? (
<div className="space-y-3" data-testid="reimbursement-approval-history">
{detail.approvalHistory.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="font-medium text-zinc-900">{workflowActionLabel(item.action)}</span>
<span className="text-xs text-zinc-500">{formatDateTime(item.createdAt)}</span>
</div>
<div className="mt-1 text-xs text-zinc-500">
{item.stepName ?? "-"} · {item.actorName || "系统"}
</div>
{item.comment ? <div className="mt-2 text-zinc-600">{item.comment}</div> : null}
</div>
))}
</div>
) : (
<div className="text-sm text-zinc-500">暂无审批记录。</div>
)}
</DetailSideMeta>
<ReimbursementActions
status={detail.status}
canPay={canPay}