AP6 增加审批催办与效率摘要
这个提交包含在:
@@ -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}
|
||||
|
||||
在新工单中引用
屏蔽一个用户