重构项目合同轻台账与详情命令面板(05-30-project-contract-ledger-detail-shell)
这个提交包含在:
@@ -5,6 +5,7 @@ import { fetchEmployees } from "../api/employees";
|
||||
import { archiveProject, createConfidentialDocumentRef, createProjectCommunicationLog, createProjectDelivery, createProjectMember, createProjectMilestone, createProjectRisk, createProjectTask, fetchProject, fetchProjectClosureChecklist, fetchProjectCommunicationLogs, fetchProjectDeliveries, fetchProjectDocumentRefs, fetchProjectExpensesSummary, fetchProjectMembers, fetchProjectMilestones, fetchProjectPurchaseSummary, fetchProjectRisks, fetchProjectTasks, fetchProjectTravelSummary, fetchProjectWeeklyReports, patchProject, patchProjectClosureChecklistItem, patchProjectCommunicationLog, patchProjectMember, patchProjectMilestone, patchProjectRisk, patchProjectTask, type ConfidentialityLevel, type DocumentStorageType, type ProjectCommunicationChannel, type ProjectClosureChecklistStatus, type ProjectDeliveryRecordType, type ProjectDetail, type ProjectMilestoneStatus, type ProjectRiskLevel, type ProjectStatus, type ProjectTaskPriority, type ProjectTaskStatus } from "../api/projects";
|
||||
import { fetchContractsRegistry } from "../api/contractsRegistry";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
|
||||
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
@@ -160,6 +161,14 @@ function resolveProjectEvidenceText(detail: ProjectDetail) {
|
||||
return `交付 ${facts.deliveryRecordCount} · 合同 ${facts.contractCount} · 资料引用 ${facts.confidentialDocRefCount}`;
|
||||
}
|
||||
|
||||
function documentLocationSummary(item: { confidentialityLevel: ConfidentialityLevel; storageType: DocumentStorageType; offlineArchiveNo: string | null }) {
|
||||
const archiveNo = item.offlineArchiveNo ? ` · ${item.offlineArchiveNo}` : "";
|
||||
if (item.confidentialityLevel === "confidential" || item.confidentialityLevel === "restricted") {
|
||||
return `${storageTypeLabel(item.storageType)} · 已登记受控索引${archiveNo}`;
|
||||
}
|
||||
return `${storageTypeLabel(item.storageType)} · 归档索引${archiveNo || " · 已登记"}`;
|
||||
}
|
||||
|
||||
export function ProjectDetailPage() {
|
||||
const { projectId = "" } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -253,6 +262,7 @@ export function ProjectDetailPage() {
|
||||
const [editingCommunicationLogId, setEditingCommunicationLogId] = useState<string | null>(null);
|
||||
const [waiveChecklistTarget, setWaiveChecklistTarget] = useState<{ itemId: string; currentNote: string | null } | null>(null);
|
||||
const [waiveChecklistNote, setWaiveChecklistNote] = useState("");
|
||||
const [confirmArchiveProject, setConfirmArchiveProject] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail) return;
|
||||
@@ -588,7 +598,7 @@ export function ProjectDetailPage() {
|
||||
<Button asChild variant="secondary" type="button">
|
||||
<Link to={`/contracts-registry?projectId=${detail.id}`}>合同台账</Link>
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" disabled={archiveProjectMutation.isPending || detail.status === "archived"} onClick={() => archiveProjectMutation.mutate()}>
|
||||
<Button type="button" variant="secondary" disabled={archiveProjectMutation.isPending || detail.status === "archived"} onClick={() => setConfirmArchiveProject(true)}>
|
||||
{archiveProjectMutation.isPending ? "归档中..." : "归档项目"}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1019,7 +1029,7 @@ export function ProjectDetailPage() {
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="归档与合规 · 涉密资料引用" description="只登记目录、链接和归档位置,正文与附件仍离线管理。">
|
||||
<SectionPanel title="归档与合规 · 资料索引" description="只登记最小必要索引,正文、源码、密钥、完整路径和附件仍走受控归档。">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Input value={docTitle} onChange={(event) => setDocTitle(event.target.value)} placeholder="资料标题" />
|
||||
@@ -1027,7 +1037,7 @@ export function ProjectDetailPage() {
|
||||
<Input value={docNo} onChange={(event) => setDocNo(event.target.value)} placeholder="文档编号(可选)" />
|
||||
<Input value={docOfflineArchiveNo} onChange={(event) => setDocOfflineArchiveNo(event.target.value)} placeholder="离线档案号(可选)" />
|
||||
</div>
|
||||
<Input value={docStoragePath} onChange={(event) => setDocStoragePath(event.target.value)} placeholder="NAS / Git / 文件盘路径或引用地址" />
|
||||
<Input value={docStoragePath} onChange={(event) => setDocStoragePath(event.target.value)} placeholder="归档位置摘要或受控索引,不填写完整路径、密钥或源码地址" />
|
||||
<div className="grid gap-3 md:grid-cols-[180px,180px,120px]">
|
||||
<select className="select-std" value={docConfidentiality} onChange={(event) => setDocConfidentiality(event.target.value as ConfidentialityLevel)}>
|
||||
{CONFIDENTIALITY_OPTIONS.map((item) => <option key={item} value={item}>{confidentialityLabel(item)}</option>)}
|
||||
@@ -1039,7 +1049,7 @@ export function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader><TableRow><TableHead>资料</TableHead><TableHead>密级</TableHead><TableHead>存储</TableHead><TableHead>路径</TableHead></TableRow></TableHeader>
|
||||
<TableHeader><TableRow><TableHead>资料</TableHead><TableHead>密级</TableHead><TableHead>存储</TableHead><TableHead>归档索引</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
{(docsQuery.data?.items ?? []).map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
@@ -1049,7 +1059,7 @@ export function ProjectDetailPage() {
|
||||
</TableCell>
|
||||
<TableCell>{confidentialityLabel(item.confidentialityLevel)}</TableCell>
|
||||
<TableCell>{storageTypeLabel(item.storageType)}</TableCell>
|
||||
<TableCell className="max-w-[280px] truncate">{item.storagePath}</TableCell>
|
||||
<TableCell className="max-w-[280px] truncate">{documentLocationSummary(item)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -1150,9 +1160,9 @@ export function ProjectDetailPage() {
|
||||
<div className="mt-1 text-xl font-semibold text-zinc-900">{contractsQuery.data?.items.length ?? 0}</div>
|
||||
<div className="mt-2 space-y-2 text-sm">
|
||||
{(contractsQuery.data?.items ?? []).slice(0, 5).map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-zinc-100 px-3 py-2">
|
||||
<Link key={item.id} to={`/contracts-registry?projectId=${detail.id}`} className="block rounded-lg border border-zinc-100 px-3 py-2 hover:bg-zinc-50">
|
||||
{item.contractNo} · {item.counterpartyName}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1224,6 +1234,15 @@ export function ProjectDetailPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ActionConfirmDialog
|
||||
open={confirmArchiveProject}
|
||||
onOpenChange={setConfirmArchiveProject}
|
||||
kind="archive"
|
||||
targetName={detail.projectNo}
|
||||
description="归档后该项目不再作为新的执行项目推进,历史成员、交付、合同、费用和资料索引仍保留。请确认当前收口项、风险和关联业务已经核对。"
|
||||
confirmLabel="确认归档"
|
||||
onConfirm={() => archiveProjectMutation.mutate()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户