From ef6392be810e20d6b16b5dc89073a490def6576b Mon Sep 17 00:00:00 2001 From: jingchuan Date: Tue, 18 Aug 2026 19:37:17 +0800 Subject: [PATCH] feat(supply-chain): improve timing detail drilldown --- .../NewProductManagementDashboard.tsx | 215 +++++++++++++++--- 1 file changed, 184 insertions(+), 31 deletions(-) diff --git a/furniture-3d-system/src/app/intake/new-product-management/NewProductManagementDashboard.tsx b/furniture-3d-system/src/app/intake/new-product-management/NewProductManagementDashboard.tsx index 19acf1b1..ee98db9a 100644 --- a/furniture-3d-system/src/app/intake/new-product-management/NewProductManagementDashboard.tsx +++ b/furniture-3d-system/src/app/intake/new-product-management/NewProductManagementDashboard.tsx @@ -128,6 +128,17 @@ type TimingOverview = { runningLongestTargetDays: number | null; }; +type CompletedTimingDetailFilter = + | { + kind: "department"; + key: CompletedDepartmentTimingSummary["key"]; + } + | { + kind: "max-overtime"; + rowId: string; + } + | null; + type BaseAnalysisDatum = (typeof ANALYSIS_DATA)[Exclude< AnalysisView, "overview" @@ -1890,6 +1901,8 @@ function EfficiencyCard({ note, tone, departmentItems, + hoverDetail, + active = false, onClick, }: { label: string; @@ -1906,8 +1919,16 @@ function EfficiencyCard({ tone: "good" | "danger"; previewRows?: Array<{ openingCode: string; productName: string }>; actionLabel?: string; + active?: boolean; onSelect?: () => void; }>; + hoverDetail?: { + title: string; + detail: string; + lines?: string[]; + actionLabel?: string; + }; + active?: boolean; onClick?: () => void; }) { const toneClass = { @@ -1922,7 +1943,7 @@ function EfficiencyCard({
{label} {onClick ? ( - + ) : null}
@@ -1949,9 +1970,11 @@ function EfficiencyCard({ ? "right-0" : "left-1/2 -translate-x-1/2"; const itemTone = - item.tone === "danger" - ? "border-[#efd3cf] bg-[#fff8f7] text-[#a84741]" - : "border-[#cfe2dd] bg-[#f4faf8] text-[#28705a]"; + item.active + ? "border-[#245f5a] bg-[#eaf4f1] text-[#245f5a] ring-1 ring-[#245f5a]/20" + : item.tone === "danger" + ? "border-[#efd3cf] bg-[#fff8f7] text-[#a84741]" + : "border-[#cfe2dd] bg-[#f4faf8] text-[#28705a]"; const itemClassName = `group/dept relative flex min-w-0 flex-col items-center rounded border px-1 py-1 outline-none focus-visible:ring-2 focus-visible:ring-[#245f5a]/30 ${item.onSelect ? "cursor-pointer hover:shadow-sm" : ""} ${itemTone}`; const itemContent = ( <> @@ -2000,6 +2023,7 @@ function EfficiencyCard({ ) : ( @@ -2174,10 +2233,11 @@ function TimingDetailTable({ } /> @@ -2896,9 +2956,8 @@ export default function NewProductManagementDashboard({ const [timingDetailMode, setTimingDetailMode] = useState< "completed" | "running" | null >(null); - const [completedDepartmentFilter, setCompletedDepartmentFilter] = useState< - CompletedDepartmentTimingSummary["key"] | null - >(null); + const [completedTimingFilter, setCompletedTimingFilter] = + useState(null); const periodRange = useMemo( () => getPeriodRange(period, anchorDate), [anchorDate, period], @@ -2947,16 +3006,50 @@ export default function NewProductManagementDashboard({ ); const selectedCompletedDepartment = useMemo( () => - timingOverview.completedDepartments.find( - (department) => department.key === completedDepartmentFilter, - ) ?? null, - [completedDepartmentFilter, timingOverview.completedDepartments], + completedTimingFilter?.kind === "department" + ? (timingOverview.completedDepartments.find( + (department) => + department.key === completedTimingFilter.key, + ) ?? null) + : null, + [completedTimingFilter, timingOverview.completedDepartments], + ); + const maxCompletedOvertimeRow = useMemo( + () => + timingDetails.completed.reduce( + (maxRow, row) => { + if (!maxRow) return row; + return (row.totalOvertimeDays ?? -1) > + (maxRow.totalOvertimeDays ?? -1) + ? row + : maxRow; + }, + null, + ), + [timingDetails.completed], ); const completedTimingDetailRows = useMemo(() => { - if (!selectedCompletedDepartment) return timingDetails.completed; - const rowIds = new Set(selectedCompletedDepartment.overtimeRowIds); - return timingDetails.completed.filter((row) => rowIds.has(row.id)); - }, [selectedCompletedDepartment, timingDetails.completed]); + if (completedTimingFilter?.kind === "max-overtime") { + return timingDetails.completed.filter( + (row) => row.id === completedTimingFilter.rowId, + ); + } + if (selectedCompletedDepartment) { + const rowIds = new Set(selectedCompletedDepartment.overtimeRowIds); + return timingDetails.completed.filter((row) => rowIds.has(row.id)); + } + return timingDetails.completed; + }, [ + completedTimingFilter, + selectedCompletedDepartment, + timingDetails.completed, + ]); + const completedTimingFilterLabel = + selectedCompletedDepartment !== null + ? `${selectedCompletedDepartment.label}超时流程` + : completedTimingFilter?.kind === "max-overtime" + ? "最大周期超期任务" + : undefined; const stageData: StageDatum[] = useMemo( () => (dashboard && liveItems ? buildLiveStageData(liveItems) : STAGE_DATA), [dashboard, liveItems], @@ -3170,7 +3263,7 @@ export default function NewProductManagementDashboard({ type="button" aria-expanded={timingDetailMode === "completed"} onClick={() => { - setCompletedDepartmentFilter(null); + setCompletedTimingFilter(null); setTimingDetailMode((current) => current === "completed" ? null : "completed", ); @@ -3235,6 +3328,10 @@ export default function NewProductManagementDashboard({ department.overtimeCount > 0 ? ("danger" as const) : ("good" as const), + active: + timingDetailMode === "completed" && + completedTimingFilter?.kind === "department" && + completedTimingFilter.key === department.key, previewRows: department.overtimeRowIds .slice(0, 4) .flatMap((rowId) => { @@ -3251,12 +3348,29 @@ export default function NewProductManagementDashboard({ : []; }), actionLabel: department.overtimeCount - ? `点击查看全部 ${department.overtimeCount} 个流程` + ? timingDetailMode === "completed" && + completedTimingFilter?.kind === "department" && + completedTimingFilter.key === department.key + ? "已展开,再次点击收起" + : `点击查看全部 ${department.overtimeCount} 个流程` : undefined, onSelect: department.overtimeCount ? () => { - setCompletedDepartmentFilter(department.key); - setTimingDetailMode("completed"); + const isActive = + timingDetailMode === "completed" && + completedTimingFilter?.kind === "department" && + completedTimingFilter.key === department.key; + setCompletedTimingFilter( + isActive + ? null + : { + kind: "department", + key: department.key, + }, + ); + setTimingDetailMode( + isActive ? null : "completed", + ); } : undefined, }), @@ -3294,20 +3408,59 @@ export default function NewProductManagementDashboard({ tone={ timingOverview.completedMaxOvertimeDays ? "danger" : "good" } + active={ + timingDetailMode === "completed" && + completedTimingFilter?.kind === "max-overtime" + } + hoverDetail={ + maxCompletedOvertimeRow && + timingOverview.completedMaxOvertimeDays > 0 + ? { + title: `最大周期超期:${timingOverview.completedMaxOvertimeDays} 天`, + detail: `${maxCompletedOvertimeRow.openingCode} · ${maxCompletedOvertimeRow.productName}`, + lines: [ + `最终时效 ${maxCompletedOvertimeRow.totalActualDays ?? "-"} 天 / 标准 ${maxCompletedOvertimeRow.standardDays ?? "-"} 天`, + `完成日期 ${maxCompletedOvertimeRow.finishedDate || "-"}`, + ], + actionLabel: + timingDetailMode === "completed" && + completedTimingFilter?.kind === "max-overtime" + ? "已展开,再次点击收起" + : "点击查看该款完整节点时效", + } + : undefined + } + onClick={ + maxCompletedOvertimeRow && + timingOverview.completedMaxOvertimeDays > 0 + ? () => { + const isActive = + timingDetailMode === "completed" && + completedTimingFilter?.kind === "max-overtime"; + setCompletedTimingFilter( + isActive + ? null + : { + kind: "max-overtime", + rowId: maxCompletedOvertimeRow.id, + }, + ); + setTimingDetailMode( + isActive ? null : "completed", + ); + } + : undefined + } />
{timingDetailMode === "completed" ? ( { setTimingDetailMode(null); - setCompletedDepartmentFilter(null); + setCompletedTimingFilter(null); }} onSelectRow={setSelectedRow} /> @@ -3328,7 +3481,7 @@ export default function NewProductManagementDashboard({ type="button" aria-expanded={timingDetailMode === "running"} onClick={() => { - setCompletedDepartmentFilter(null); + setCompletedTimingFilter(null); setTimingDetailMode((current) => current === "running" ? null : "running", );