feat: enhance ledger archive and approval sla

This commit is contained in:
cyuqei
2026-06-19 13:31:49 +08:00
parent ae76b9a296
commit 52a676ff19
2 changed files with 578 additions and 8 deletions
+241 -4
View File
@@ -2,7 +2,9 @@
import {
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
FileSearchOutlined,
ReloadOutlined,
} from "@ant-design/icons";
@@ -43,6 +45,100 @@ const statusOptions = [
{ label: "已退回", value: "rejected" },
];
const slaOptions = [
{ label: "全部时效", value: "all" },
{ label: "已超时", value: "overdue" },
{ label: "临近超时", value: "due_soon" },
{ label: "SLA 内", value: "normal" },
{ label: "已处理", value: "closed" },
];
const APPROVAL_SLA_HOURS = 48;
const APPROVAL_SLA_WARNING_HOURS = 24;
type ApprovalSlaStatus = "normal" | "due_soon" | "overdue" | "closed";
function getTimeValue(value: string | null | undefined) {
if (!value) return 0;
const time = new Date(value).getTime();
return Number.isNaN(time) ? 0 : time;
}
function formatDurationHours(hours: number) {
if (!Number.isFinite(hours) || hours <= 0) return "不足1小时";
const rounded = Math.floor(hours);
if (rounded < 24) return `${Math.max(1, rounded)}小时`;
const days = Math.floor(rounded / 24);
const restHours = rounded % 24;
return restHours > 0 ? `${days}${restHours}小时` : `${days}`;
}
function buildApprovalSla(record: ContractApprovalStep): {
status: ApprovalSlaStatus;
label: string;
color: string;
elapsedHours: number;
dueAt: string | null;
description: string;
} {
const createdAt = getTimeValue(record.created_at);
const finishedAt = getTimeValue(record.decided_at);
const endAt = record.status === "pending" ? Date.now() : finishedAt || Date.now();
const elapsedHours = createdAt ? Math.max((endAt - createdAt) / 3_600_000, 0) : 0;
const dueAt = createdAt ? new Date(createdAt + APPROVAL_SLA_HOURS * 3_600_000).toISOString() : null;
if (record.status !== "pending") {
return {
status: "closed",
label: "已处理",
color: "default",
elapsedHours,
dueAt,
description: `处理耗时 ${formatDurationHours(elapsedHours)}`,
};
}
if (elapsedHours >= APPROVAL_SLA_HOURS) {
return {
status: "overdue",
label: "已超时",
color: "red",
elapsedHours,
dueAt,
description: `已等待 ${formatDurationHours(elapsedHours)}`,
};
}
if (elapsedHours >= APPROVAL_SLA_WARNING_HOURS) {
const remainingHours = APPROVAL_SLA_HOURS - elapsedHours;
return {
status: "due_soon",
label: "临近超时",
color: "orange",
elapsedHours,
dueAt,
description: `剩余 ${formatDurationHours(remainingHours)}`,
};
}
return {
status: "normal",
label: "SLA 内",
color: "green",
elapsedHours,
dueAt,
description: `已等待 ${formatDurationHours(elapsedHours)}`,
};
}
function getApprovalPriorityWeight(record: ContractApprovalStep) {
const sla = buildApprovalSla(record);
if (record.status !== "pending") return 99;
if (sla.status === "overdue") return 0;
if (sla.status === "due_soon") return 1;
return 2;
}
export default function ContractApprovalsPage() {
const [messageApi, contextHolder] = message.useMessage();
const [items, setItems] = useState<ContractApprovalStep[]>([]);
@@ -50,6 +146,7 @@ export default function ContractApprovalsPage() {
const [loading, setLoading] = useState(true);
const [scope, setScope] = useState("assigned_to_me");
const [statusValue, setStatusValue] = useState("");
const [slaFilter, setSlaFilter] = useState("all");
const [decidingId, setDecidingId] = useState<string | null>(null);
const loadApprovals = useCallback(async () => {
@@ -81,13 +178,41 @@ export default function ContractApprovalsPage() {
return () => window.clearTimeout(timer);
}, [loadApprovals]);
const filteredItems = useMemo(() => {
if (slaFilter === "all") return items;
return items.filter((item) => buildApprovalSla(item).status === slaFilter);
}, [items, slaFilter]);
const priorityItems = useMemo(() => {
return items
.filter((item) => item.status === "pending")
.sort((a, b) => {
const weightDiff = getApprovalPriorityWeight(a) - getApprovalPriorityWeight(b);
if (weightDiff !== 0) return weightDiff;
return getTimeValue(a.created_at) - getTimeValue(b.created_at);
})
.slice(0, 5);
}, [items]);
const slaSummary = useMemo(() => {
const pendingItems = items.filter((item) => item.status === "pending");
return {
overdue: pendingItems.filter((item) => buildApprovalSla(item).status === "overdue").length,
dueSoon: pendingItems.filter((item) => buildApprovalSla(item).status === "due_soon").length,
normal: pendingItems.filter((item) => buildApprovalSla(item).status === "normal").length,
};
}, [items]);
const metrics = useMemo(() => [
{ label: "待我审批", value: summary.assigned_to_me ?? 0, tone: "warning" },
{ label: "全部待审批", value: summary.pending_all ?? 0, tone: "warning" },
{ label: "已超时", value: slaSummary.overdue, tone: "danger" },
{ label: "临近超时", value: slaSummary.dueSoon, tone: "warning" },
{ label: "SLA 内", value: slaSummary.normal, tone: "success" },
{ label: "我提交的", value: summary.submitted_by_me ?? 0 },
{ label: "已通过", value: summary.approved ?? 0, tone: "success" },
{ label: "已退回", value: summary.rejected ?? 0, tone: "danger" },
], [summary]);
], [slaSummary.dueSoon, slaSummary.normal, slaSummary.overdue, summary]);
const handleDecision = useCallback((item: ContractApprovalStep, decision: "approve" | "reject") => {
Modal.confirm({
@@ -179,6 +304,21 @@ export default function ContractApprovalsPage() {
render: (value: string | null) => formatDateTime(value),
sorter: (a, b) => new Date(a.created_at || 0).getTime() - new Date(b.created_at || 0).getTime(),
},
{
title: "审批时效",
key: "sla",
width: 190,
render: (_, record) => {
const sla = buildApprovalSla(record);
return (
<Space direction="vertical" size={2}>
<Tag color={sla.color}>{sla.label}</Tag>
<Text type="secondary">{sla.description}</Text>
</Space>
);
},
sorter: (a, b) => getApprovalPriorityWeight(a) - getApprovalPriorityWeight(b),
},
{
title: "操作",
key: "actions",
@@ -244,26 +384,123 @@ export default function ContractApprovalsPage() {
</div>
</section>
<section className="landing-section">
<div className="landing-section-header contract-ledger-list-header">
<div>
<h2></h2>
<p> SLA </p>
</div>
</div>
{priorityItems.length === 0 ? (
<Empty description={loading ? "审批队列加载中..." : "当前没有待处理审批。"} />
) : (
<div className="contract-ledger-table-shell">
<Table
dataSource={priorityItems}
loading={loading}
pagination={false}
rowKey="id"
scroll={{ x: 980 }}
columns={[
{
title: "合同",
key: "contract",
render: (_, record) => (
<Space direction="vertical" size={2}>
<Link href={`/contracts/${record.contract_id}`}>
<Text strong>{record.contract?.template_name ?? record.title}</Text>
</Link>
<Text type="secondary">{record.contract?.file_name ?? "合同文件"}</Text>
</Space>
),
},
{
title: "时效",
key: "sla",
width: 180,
render: (_, record) => {
const sla = buildApprovalSla(record);
return (
<Space direction="vertical" size={2}>
<Tag color={sla.color} icon={sla.status === "overdue" ? <ExclamationCircleOutlined /> : <ClockCircleOutlined />}>
{sla.label}
</Tag>
<Text type="secondary">{sla.description}</Text>
</Space>
);
},
},
{
title: "提交/处理人",
key: "people",
width: 210,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Text>{record.submitter_name || "提交人未记录"}</Text>
<Text type="secondary">{record.assignee_name || "-"}</Text>
</Space>
),
},
{
title: "截止参考",
key: "due",
width: 180,
render: (_, record) => <Text>{formatDateTime(buildApprovalSla(record).dueAt)}</Text>,
},
{
title: "操作",
key: "actions",
width: 210,
render: (_, record) => (
<Space wrap>
<Link href={`/contracts/${record.contract_id}`}></Link>
<Button
size="small"
icon={<CheckCircleOutlined />}
loading={decidingId === record.id}
onClick={() => handleDecision(record, "approve")}
>
</Button>
<Button
danger
size="small"
icon={<CloseCircleOutlined />}
loading={decidingId === record.id}
onClick={() => handleDecision(record, "reject")}
>
退
</Button>
</Space>
),
},
]}
/>
</div>
)}
</section>
<section className="landing-section">
<div className="landing-section-header contract-ledger-list-header">
<div>
<h2></h2>
<p> {items.length} </p>
<p> {filteredItems.length} {items.length} </p>
</div>
<Space wrap>
<Select options={scopeOptions} value={scope} onChange={setScope} style={{ width: 140 }} />
<Select options={statusOptions} value={statusValue} onChange={setStatusValue} style={{ width: 140 }} />
<Select options={slaOptions} value={slaFilter} onChange={setSlaFilter} style={{ width: 140 }} />
</Space>
</div>
<div className="contract-ledger-table-shell">
<Table
columns={columns}
dataSource={items}
dataSource={filteredItems}
loading={loading}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无审批任务。" /> }}
pagination={{ pageSize: 10 }}
rowKey="id"
scroll={{ x: 1330 }}
scroll={{ x: 1520 }}
/>
</div>
</section>
+337 -4
View File
@@ -1,13 +1,17 @@
"use client";
import {
CheckCircleOutlined,
DownloadOutlined,
FileDoneOutlined,
InboxOutlined,
ClockCircleOutlined,
ReloadOutlined,
SearchOutlined,
StopOutlined,
WarningOutlined,
} from "@ant-design/icons";
import { Button, Empty, Input, Select, Space, Table, Tag, Typography, message } from "antd";
import { Button, Empty, Input, Popconfirm, Select, Space, Table, Tag, Typography, message } from "antd";
import type { ColumnsType } from "antd/es/table";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -36,6 +40,9 @@ import {
import {
listContractLifecycleTaskWorkbench,
listGeneratedContracts,
createContractLifecycleTask,
updateGeneratedContractLifecycle,
updateContractLifecycleTask,
type ContractLifecycleTask,
type GeneratedContractListItem,
} from "@/services/contract";
@@ -48,6 +55,17 @@ type LedgerMetric = {
tone?: "warning" | "danger" | "success";
};
type ArchiveReadinessStatus = "needs_task" | "in_progress" | "needs_evidence" | "ready_to_archive" | "archived" | "monitoring";
type ArchiveReadinessItem = {
contract: GeneratedContractListItem;
task?: ContractLifecycleTask;
status: ArchiveReadinessStatus;
label: string;
color: string;
description: string;
};
const ALL_FILTER = "__all__";
const dueStatusOptions = [
@@ -77,6 +95,27 @@ const riskStatusOptions = [
{ value: "unrated", label: "未评级" },
];
const lifecycleTaskActionMessages: Record<string, string> = {
in_progress: "提醒已设为跟进中",
completed: "提醒已标记完成",
skipped: "提醒已跳过",
archived: "提醒已归档",
};
const archiveReadinessMeta: Record<ArchiveReadinessStatus, { label: string; color: string; description: string }> = {
needs_task: { label: "需建归档任务", color: "gold", description: "合同已接近关闭节点,需要补齐终止、结算或签署归档任务。" },
in_progress: { label: "归档处理中", color: "blue", description: "已有归档任务,继续收集签署文本、验收或终止确认材料。" },
needs_evidence: { label: "待补证据", color: "orange", description: "归档任务已完成,但还没有绑定归档证据文件。" },
ready_to_archive: { label: "可归档", color: "green", description: "归档任务与证据已就绪,可将合同生命周期更新为已归档。" },
archived: { label: "已归档", color: "default", description: "合同已完成归档。" },
monitoring: { label: "持续跟踪", color: "default", description: "合同仍在履约或签署阶段,暂不需要归档处理。" },
};
const archiveLifecycleStatuses = new Set(["signed", "effective", "expiring", "expired", "terminated", "archived"]);
const archiveTaskRequiredLifecycleStatuses = new Set(["signed", "expired", "terminated"]);
const urgentArchiveLifecycleStatuses = new Set(["expired", "terminated"]);
const closedLifecycleTaskStatuses = new Set(["completed", "skipped", "archived"]);
function getLifecycleStatus(item: GeneratedContractListItem) {
return item.lifecycle?.status ?? "generated";
}
@@ -139,6 +178,66 @@ function getPriorityDescription(item: GeneratedContractListItem) {
return "继续完善合同状态和后续处理记录。";
}
function isArchiveCandidate(item: GeneratedContractListItem) {
return archiveLifecycleStatuses.has(getLifecycleStatus(item)) || ["due_soon", "overdue"].includes(getDueStatus(item));
}
function getLatestArchiveTask(tasks: ContractLifecycleTask[], contractId: string) {
return tasks
.filter((task) => task.contract_id === contractId && task.task_type === "termination_archive")
.sort((a, b) => getTimeValue(b.updated_at ?? b.created_at ?? b.due_at) - getTimeValue(a.updated_at ?? a.created_at ?? a.due_at))[0];
}
function buildArchiveReadinessItem(contract: GeneratedContractListItem, tasks: ContractLifecycleTask[]): ArchiveReadinessItem {
const task = getLatestArchiveTask(tasks, contract.id);
const lifecycleStatus = getLifecycleStatus(contract);
const dueStatus = getDueStatus(contract);
let status: ArchiveReadinessStatus = "monitoring";
if (lifecycleStatus === "archived") {
status = "archived";
} else if (task?.status === "completed") {
status = task.evidence_file_id || task.evidence_file ? "ready_to_archive" : "needs_evidence";
} else if (task && !closedLifecycleTaskStatuses.has(task.status)) {
status = "in_progress";
} else if (archiveTaskRequiredLifecycleStatuses.has(lifecycleStatus) || dueStatus === "overdue" || dueStatus === "due_soon") {
status = "needs_task";
}
const meta = archiveReadinessMeta[status];
return { contract, task, status, ...meta };
}
function buildArchiveReadinessItems(contracts: GeneratedContractListItem[], tasks: ContractLifecycleTask[]) {
return contracts
.filter(isArchiveCandidate)
.map((contract) => buildArchiveReadinessItem(contract, tasks))
.filter((item) => item.status !== "monitoring" && item.status !== "archived")
.sort((a, b) => {
const weightMap: Record<ArchiveReadinessStatus, number> = {
needs_task: 0,
needs_evidence: 1,
ready_to_archive: 2,
in_progress: 3,
monitoring: 9,
archived: 10,
};
const weightDiff = weightMap[a.status] - weightMap[b.status];
if (weightDiff !== 0) return weightDiff;
return getTimeValue(a.contract.lifecycle?.expires_at ?? a.contract.generated_at) - getTimeValue(b.contract.lifecycle?.expires_at ?? b.contract.generated_at);
});
}
function buildArchiveMetrics(contracts: GeneratedContractListItem[], tasks: ContractLifecycleTask[]): LedgerMetric[] {
const items = contracts.filter(isArchiveCandidate).map((contract) => buildArchiveReadinessItem(contract, tasks));
return [
{ label: "归档关注", value: items.filter((item) => !["archived", "monitoring"].includes(item.status)).length, tone: "warning" },
{ label: "归档处理中", value: items.filter((item) => item.status === "in_progress").length, tone: "warning" },
{ label: "证据待补", value: items.filter((item) => item.status === "needs_evidence").length, tone: "danger" },
{ label: "可归档", value: items.filter((item) => item.status === "ready_to_archive").length, tone: "success" },
];
}
function buildMetrics(items: GeneratedContractListItem[]): LedgerMetric[] {
const inReview = items.filter((item) => ["internal_review", "external_review"].includes(getLifecycleStatus(item))).length;
const active = items.filter((item) => ["signed", "effective", "expiring"].includes(getLifecycleStatus(item))).length;
@@ -203,6 +302,8 @@ export default function ContractLedgerPage() {
const [lifecycleTasks, setLifecycleTasks] = useState<ContractLifecycleTask[]>([]);
const [loading, setLoading] = useState(true);
const [taskLoading, setTaskLoading] = useState(true);
const [updatingTaskAction, setUpdatingTaskAction] = useState<string | null>(null);
const [archiveActionKey, setArchiveActionKey] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [lifecycleFilter, setLifecycleFilter] = useState(ALL_FILTER);
const [dueFilter, setDueFilter] = useState(ALL_FILTER);
@@ -262,6 +363,8 @@ export default function ContractLedgerPage() {
}, [approvalFilter, contracts, dueFilter, lifecycleFilter, riskFilter, searchQuery]);
const metrics = useMemo(() => buildMetrics(filteredContracts), [filteredContracts]);
const archiveMetrics = useMemo(() => buildArchiveMetrics(filteredContracts, lifecycleTasks), [filteredContracts, lifecycleTasks]);
const archiveReadinessItems = useMemo(() => buildArchiveReadinessItems(filteredContracts, lifecycleTasks).slice(0, 6), [filteredContracts, lifecycleTasks]);
const recentContracts = useMemo(() => {
return [...filteredContracts]
.sort((a, b) => getTimeValue(b.lifecycle?.updated_at ?? b.generated_at) - getTimeValue(a.lifecycle?.updated_at ?? a.generated_at))
@@ -309,6 +412,86 @@ export default function ContractLedgerPage() {
URL.revokeObjectURL(url);
};
const handleUpdateLifecycleTaskStatus = useCallback(
async (task: ContractLifecycleTask, statusValue: string) => {
const actionKey = `${task.id}:${statusValue}`;
setUpdatingTaskAction(actionKey);
try {
const result = await updateContractLifecycleTask(task.contract_id, task.id, { status_value: statusValue });
if (result.code !== 0) {
messageApi.error(result.message || "提醒状态更新失败");
return;
}
messageApi.success(lifecycleTaskActionMessages[statusValue] ?? "提醒状态已更新");
await Promise.all([loadLifecycleTasks(), loadContracts()]);
} catch {
messageApi.error("提醒状态更新失败,请稍后重试");
} finally {
setUpdatingTaskAction(null);
}
},
[loadContracts, loadLifecycleTasks, messageApi],
);
const handleCreateArchiveTask = useCallback(
async (contract: GeneratedContractListItem) => {
const actionKey = `${contract.id}:create-archive-task`;
setArchiveActionKey(actionKey);
try {
const result = await createContractLifecycleTask(contract.id, {
task_type: "termination_archive",
title: `归档准备:${contract.template_name}`,
description: "收集签署文件、验收材料、终止确认、结算记录或续约判断依据。",
priority: getDueStatus(contract) === "overdue" || urgentArchiveLifecycleStatuses.has(getLifecycleStatus(contract)) ? "high" : "medium",
status_value: "open",
due_at: contract.lifecycle?.expires_at ?? null,
});
if (result.code !== 0) {
messageApi.error(result.message || "归档任务创建失败");
return;
}
messageApi.success("归档任务已创建");
await loadLifecycleTasks();
} catch {
messageApi.error("归档任务创建失败,请稍后重试");
} finally {
setArchiveActionKey(null);
}
},
[loadLifecycleTasks, messageApi],
);
const handleArchiveContract = useCallback(
async (item: ArchiveReadinessItem) => {
const actionKey = `${item.contract.id}:archive-contract`;
setArchiveActionKey(actionKey);
try {
const result = await updateGeneratedContractLifecycle(item.contract.id, {
lifecycle_status: "archived",
counterparty_name: item.contract.lifecycle?.counterparty_name ?? null,
owner_name: item.contract.lifecycle?.owner_name ?? null,
effective_at: item.contract.lifecycle?.effective_at ?? null,
expires_at: item.contract.lifecycle?.expires_at ?? null,
lifecycle_note: item.task?.evidence_note ?? item.contract.lifecycle?.note ?? "归档任务和证据已完成,合同已归档。",
});
if (result.code !== 0) {
messageApi.error(result.message || "合同归档失败");
return;
}
if (item.task && item.task.status !== "archived") {
await updateContractLifecycleTask(item.contract.id, item.task.id, { status_value: "archived" });
}
messageApi.success("合同已归档");
await Promise.all([loadContracts(), loadLifecycleTasks()]);
} catch {
messageApi.error("合同归档失败,请稍后重试");
} finally {
setArchiveActionKey(null);
}
},
[loadContracts, loadLifecycleTasks, messageApi],
);
const columns = useMemo<ColumnsType<GeneratedContractListItem>>(() => [
{
title: "合同",
@@ -445,7 +628,7 @@ export default function ContractLedgerPage() {
</div>
<div className="contract-ledger-metric-grid">
{metrics.map((metric) => (
{[...metrics, ...archiveMetrics].map((metric) => (
<div className={`contract-ledger-metric contract-ledger-metric-${metric.tone ?? "default"}`} key={metric.label}>
<span>{metric.label}</span>
<strong>{metric.value}</strong>
@@ -526,8 +709,158 @@ export default function ContractLedgerPage() {
{
title: "操作",
key: "actions",
width: 110,
render: (_, record) => <Link href={`/contracts/${record.contract_id}`}></Link>,
width: 260,
render: (_, record) => (
<Space wrap>
<Button
disabled={record.status === "in_progress"}
icon={<ClockCircleOutlined />}
loading={updatingTaskAction === `${record.id}:in_progress`}
onClick={() => void handleUpdateLifecycleTaskStatus(record, "in_progress")}
size="small"
>
</Button>
<Button
icon={<CheckCircleOutlined />}
loading={updatingTaskAction === `${record.id}:completed`}
onClick={() => void handleUpdateLifecycleTaskStatus(record, "completed")}
size="small"
type="primary"
>
</Button>
<Button
icon={<StopOutlined />}
loading={updatingTaskAction === `${record.id}:skipped`}
onClick={() => void handleUpdateLifecycleTaskStatus(record, "skipped")}
size="small"
>
</Button>
<Popconfirm
cancelText="取消"
okText="归档"
onConfirm={() => void handleUpdateLifecycleTaskStatus(record, "archived")}
title="归档后该提醒将从待处理列表移除,确认归档?"
>
<Button
icon={<InboxOutlined />}
loading={updatingTaskAction === `${record.id}:archived`}
size="small"
>
</Button>
</Popconfirm>
<Link href={`/contracts/${record.contract_id}`}></Link>
</Space>
),
},
]}
/>
</div>
)}
</section>
<section className="landing-section">
<div className="landing-section-header">
<h2></h2>
<p></p>
</div>
{archiveReadinessItems.length === 0 ? (
<Empty description={taskLoading ? "归档状态加载中..." : "当前没有需要归档处理的合同。"} />
) : (
<div className="contract-ledger-table-shell">
<Table
rowKey={(item) => `${item.contract.id}:${item.task?.id ?? item.status}`}
dataSource={archiveReadinessItems}
loading={loading || taskLoading}
pagination={false}
columns={[
{
title: "合同",
key: "contract",
render: (_, item) => (
<Space direction="vertical" size={2}>
<Link href={`/contracts/${item.contract.id}`}>
<Text strong>{item.contract.template_name}</Text>
</Link>
<Text type="secondary">{item.contract.lifecycle?.counterparty_name || item.contract.file_name}</Text>
</Space>
),
},
{
title: "归档状态",
key: "status",
width: 160,
render: (_, item) => <Tag color={item.color}>{item.label}</Tag>,
},
{
title: "任务/证据",
key: "task",
width: 260,
render: (_, item) => (
<Space direction="vertical" size={2}>
{item.task ? (
<>
<Text>{item.task.title}</Text>
<Space wrap>
<Tag color={getLifecycleTaskStatusColor(item.task.status)}>
{formatLifecycleTaskStatus(item.task.status)}
</Tag>
<Tag color={item.task.evidence_file_id || item.task.evidence_file ? "green" : "default"}>
{item.task.evidence_file_id || item.task.evidence_file ? "已绑定证据" : "未绑定证据"}
</Tag>
</Space>
</>
) : (
<Text type="secondary"></Text>
)}
</Space>
),
},
{
title: "建议动作",
key: "description",
render: (_, item) => <Text type="secondary">{item.description}</Text>,
},
{
title: "操作",
key: "actions",
width: 240,
render: (_, item) => (
<Space wrap>
{item.status === "needs_task" ? (
<Button
icon={<FileDoneOutlined />}
loading={archiveActionKey === `${item.contract.id}:create-archive-task`}
onClick={() => void handleCreateArchiveTask(item.contract)}
size="small"
type="primary"
>
</Button>
) : null}
{item.status === "ready_to_archive" ? (
<Popconfirm
cancelText="取消"
okText="归档"
onConfirm={() => void handleArchiveContract(item)}
title="确认将该合同生命周期更新为已归档?"
>
<Button
icon={<InboxOutlined />}
loading={archiveActionKey === `${item.contract.id}:archive-contract`}
size="small"
type="primary"
>
</Button>
</Popconfirm>
) : null}
<Link href={`/contracts/${item.contract.id}`}></Link>
</Space>
),
},
]}
/>