重构项目合同轻台账与详情命令面板(05-30-project-contract-ledger-detail-shell)
这个提交包含在:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户