重构项目合同轻台账与详情命令面板(05-30-project-contract-ledger-detail-shell)

这个提交包含在:
2026-05-30 12:50:03 +08:00
父节点 dd1b655122
当前提交 fed797d19b
共修改 6 个文件,包含 253 行新增和 128 行删除
+99 -43
查看文件
@@ -21,6 +21,7 @@ import { fetchProjects, type ConfidentialityLevel } from "../api/projects";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
@@ -205,6 +206,9 @@ export function ContractsRegistryPage() {
const [editor, setEditor] = useState<EditorState>(() => buildEmptyEditor(initialProjectId));
const [showExpiring, setShowExpiring] = useState(false);
const [showTrialData, setShowTrialData] = useState(false);
const [showLedgerTools, setShowLedgerTools] = useState(false);
const [confirmExportFormat, setConfirmExportFormat] = useState<ContractRegistryExportFormat | null>(null);
const [confirmStatusTarget, setConfirmStatusTarget] = useState<ContractRegistryItem | null>(null);
const filterPayload = buildFiltersPayload(filters);
const contractsQuery = useQuery({
@@ -263,6 +267,12 @@ export function ContractsRegistryPage() {
setEditor(buildEmptyEditor(filters.projectId));
};
const openEditor = (item: ContractRegistryItem) => {
setShowLedgerTools(true);
setEditorId(item.id);
setEditor(loadEditorFromItem(item));
};
const saveMutation = useMutation({
mutationFn: async () => {
const payload = buildSavePayload(editor, { includeOwnerEmployeeId: canBrowseEmployees });
@@ -325,25 +335,12 @@ export function ContractsRegistryPage() {
}
actions={
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="secondary"
disabled={exportMutation.isPending}
onClick={() => exportMutation.mutate("csv")}
>
导出 CSV
</Button>
<Button
type="button"
variant="secondary"
disabled={exportMutation.isPending}
onClick={() => exportMutation.mutate("xlsx")}
>
导出 Excel
</Button>
<Button asChild variant="secondary" type="button">
<Link to="/projects">返回项目</Link>
</Button>
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((value) => !value)}>
台账工具
</Button>
</div>
}
/>
@@ -400,10 +397,7 @@ export function ContractsRegistryPage() {
type="button"
size="sm"
variant="secondary"
onClick={() => {
setEditorId(item.id);
setEditor(loadEditorFromItem(item));
}}
onClick={() => openEditor(item)}
>
编辑
</Button>
@@ -417,8 +411,47 @@ export function ContractsRegistryPage() {
) : null}
</div>
<div className="grid gap-6 xl:grid-cols-[400px,minmax(0,1fr)]">
<div className="card-std space-y-4 p-6">
{showLedgerTools ? (
<div className="detail-command-panel space-y-4" data-testid="contracts-ledger-tools">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
<p className="mt-1 text-sm leading-6 text-zinc-500">
新建/编辑、导出和高级到期范围放在这里。导出文件可能包含合同金额、相对方、责任人和归档摘要。
</p>
</div>
<div className="flex shrink-0 flex-wrap gap-2">
<Button type="button" variant="secondary" disabled={exportMutation.isPending} onClick={() => setConfirmExportFormat("csv")}>
{exportMutation.isPending ? "导出中..." : "导出 CSV"}
</Button>
<Button type="button" variant="secondary" disabled={exportMutation.isPending} onClick={() => setConfirmExportFormat("xlsx")}>
{exportMutation.isPending ? "导出中..." : "导出 Excel"}
</Button>
</div>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="space-y-2">
<Label>到期日开始</Label>
<DateTextInput
value={filters.expiryDateFrom}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateFrom: event.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>到期日结束</Label>
<DateTextInput
value={filters.expiryDateTo}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateTo: event.target.value }))}
/>
</div>
</div>
</div>
) : null}
<div className={showLedgerTools || editorId ? "grid gap-6 xl:grid-cols-[400px,minmax(0,1fr)]" : "grid gap-6"}>
{showLedgerTools || editorId ? (
<div className="card-std space-y-4 p-6" data-testid="contracts-editor-panel">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">{editorId ? "编辑合同台账" : "新建合同台账"}</h3>
@@ -662,6 +695,7 @@ export function ContractsRegistryPage() {
{saveMutation.isPending ? (editorId ? "保存中..." : "创建中...") : editorId ? "保存修改" : "创建合同台账"}
</Button>
</div>
) : null}
<div className="card-std space-y-4 p-6">
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
@@ -670,6 +704,11 @@ export function ContractsRegistryPage() {
<div className="mt-1 text-sm text-zinc-500">{filters.projectId ? "已按项目过滤" : "显示全部可见合同"}</div>
</div>
<div className="flex flex-wrap gap-2">
{!showLedgerTools && !editorId ? (
<Button type="button" variant="secondary" onClick={() => setShowLedgerTools(true)}>
新建合同
</Button>
) : null}
<Button
type="button"
variant="secondary"
@@ -688,9 +727,11 @@ export function ContractsRegistryPage() {
>
重置筛选
</Button>
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
{showLedgerTools ? (
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
) : null}
</div>
</div>
@@ -752,21 +793,6 @@ export function ContractsRegistryPage() {
/>
</div>
<div className="space-y-2">
<Label>到期日开始</Label>
<DateTextInput
value={filters.expiryDateFrom}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateFrom: event.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>到期日结束</Label>
<DateTextInput
value={filters.expiryDateTo}
onChange={(event) => setFilters((current) => ({ ...current, expiryDateTo: event.target.value }))}
/>
</div>
</div>
{contractsQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
@@ -815,8 +841,7 @@ export function ContractsRegistryPage() {
size="sm"
variant="secondary"
onClick={() => {
setEditorId(item.id);
setEditor(loadEditorFromItem(item));
openEditor(item);
}}
>
编辑
@@ -840,7 +865,7 @@ export function ContractsRegistryPage() {
size="sm"
variant="secondary"
disabled={toggleStatusMutation.isPending}
onClick={() => toggleStatusMutation.mutate(item)}
onClick={() => setConfirmStatusTarget(item)}
>
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
</Button>
@@ -855,6 +880,37 @@ export function ContractsRegistryPage() {
</div>
</div>
</div>
<ActionConfirmDialog
open={Boolean(confirmExportFormat)}
onOpenChange={(open) => {
if (!open) setConfirmExportFormat(null);
}}
kind="sensitiveExport"
targetName="合同台账"
description="导出文件可能包含合同金额、相对方、项目归属、责任人和归档位置摘要。请确认当前筛选范围和用途合规。"
onConfirm={() => {
if (!confirmExportFormat) return;
exportMutation.mutate(confirmExportFormat);
}}
/>
<ActionConfirmDialog
open={Boolean(confirmStatusTarget)}
onOpenChange={(open) => {
if (!open) setConfirmStatusTarget(null);
}}
kind={confirmStatusTarget?.persistedStatus === "archived" ? "enable" : "archive"}
targetName={confirmStatusTarget?.contractNo}
description={
confirmStatusTarget?.persistedStatus === "archived"
? "恢复后该合同会重新作为生效台账参与到期提醒和项目关联摘要。"
: "归档后该合同不再作为生效合同推进,台账、项目关联和离线归档索引仍保留。"
}
confirmLabel={confirmStatusTarget?.persistedStatus === "archived" ? "确认恢复" : "确认归档"}
onConfirm={() => {
if (!confirmStatusTarget) return;
toggleStatusMutation.mutate(confirmStatusTarget);
}}
/>
</PageScaffold>
);
}