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 ee98db9a..45b3bc08 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 @@ -123,9 +123,12 @@ type TimingOverview = { runningCount: number; runningOvertimeCount: number; runningNearOvertimeCount: number; + runningDepartments: CompletedDepartmentTimingSummary[]; + runningNearOvertimeRowIds: string[]; runningLongestDays: number; runningLongestTask: string; runningLongestTargetDays: number | null; + runningLongestRowId: string | null; }; type CompletedTimingDetailFilter = @@ -139,6 +142,20 @@ type CompletedTimingDetailFilter = } | null; +type RunningTimingDetailFilter = + | { + kind: "department"; + key: CompletedDepartmentTimingSummary["key"]; + } + | { + kind: "near-overtime"; + } + | { + kind: "longest"; + rowId: string; + } + | null; + type BaseAnalysisDatum = (typeof ANALYSIS_DATA)[Exclude< AnalysisView, "overview" @@ -1377,9 +1394,15 @@ function buildTimingOverview( runningCount: runningRows.length, runningOvertimeCount: runningOvertimeRows.length, runningNearOvertimeCount: runningNearOvertimeRows.length, + runningDepartments: summarizeCompletedDepartmentTimings( + runningRows, + runningOvertimeRows, + ), + runningNearOvertimeRowIds: runningNearOvertimeRows.map((row) => row.id), runningLongestDays: longestRunningRow?.totalActualDays ?? 0, runningLongestTask: longestRunningRow?.openingCode ?? "暂无进行中任务", runningLongestTargetDays: longestRunningRow?.standardDays ?? null, + runningLongestRowId: longestRunningRow?.id ?? null, }; } @@ -2958,6 +2981,8 @@ export default function NewProductManagementDashboard({ >(null); const [completedTimingFilter, setCompletedTimingFilter] = useState(null); + const [runningTimingFilter, setRunningTimingFilter] = + useState(null); const periodRange = useMemo( () => getPeriodRange(period, anchorDate), [anchorDate, period], @@ -3050,6 +3075,44 @@ export default function NewProductManagementDashboard({ : completedTimingFilter?.kind === "max-overtime" ? "最大周期超期任务" : undefined; + const selectedRunningDepartment = useMemo( + () => + runningTimingFilter?.kind === "department" + ? (timingOverview.runningDepartments.find( + (department) => department.key === runningTimingFilter.key, + ) ?? null) + : null, + [runningTimingFilter, timingOverview.runningDepartments], + ); + const runningTimingDetailRows = useMemo(() => { + if (runningTimingFilter?.kind === "near-overtime") { + const rowIds = new Set(timingOverview.runningNearOvertimeRowIds); + return timingDetails.running.filter((row) => rowIds.has(row.id)); + } + if (runningTimingFilter?.kind === "longest") { + return timingDetails.running.filter( + (row) => row.id === runningTimingFilter.rowId, + ); + } + if (selectedRunningDepartment) { + const rowIds = new Set(selectedRunningDepartment.overtimeRowIds); + return timingDetails.running.filter((row) => rowIds.has(row.id)); + } + return timingDetails.running; + }, [ + runningTimingFilter, + selectedRunningDepartment, + timingDetails.running, + timingOverview.runningNearOvertimeRowIds, + ]); + const runningTimingFilterLabel = + selectedRunningDepartment !== null + ? `${selectedRunningDepartment.label}超时流程` + : runningTimingFilter?.kind === "near-overtime" + ? "临近超期任务" + : runningTimingFilter?.kind === "longest" + ? "最长已用时任务" + : undefined; const stageData: StageDatum[] = useMemo( () => (dashboard && liveItems ? buildLiveStageData(liveItems) : STAGE_DATA), [dashboard, liveItems], @@ -3264,6 +3327,7 @@ export default function NewProductManagementDashboard({ aria-expanded={timingDetailMode === "completed"} onClick={() => { setCompletedTimingFilter(null); + setRunningTimingFilter(null); setTimingDetailMode((current) => current === "completed" ? null : "completed", ); @@ -3506,7 +3570,7 @@ export default function NewProductManagementDashboard({ tone="neutral" /> ({ + label: department.label, + value: `${department.overtimeCount}个`, + tooltipTitle: `${department.label}超时流程:${department.overtimeCount} 个`, + tooltipDetail: `进行中流程中,${department.overtimeParts + .map((part) => `${part.label} ${part.count} 个`) + .join(";")};同一流程不重复计数`, + tone: + department.overtimeCount > 0 + ? ("danger" as const) + : ("good" as const), + active: + timingDetailMode === "running" && + runningTimingFilter?.kind === "department" && + runningTimingFilter.key === department.key, + previewRows: department.overtimeRowIds + .slice(0, 4) + .flatMap((rowId) => { + const row = timingDetails.running.find( + (candidate) => candidate.id === rowId, + ); + return row + ? [ + { + openingCode: row.openingCode, + productName: row.productName, + }, + ] + : []; + }), + actionLabel: department.overtimeCount + ? timingDetailMode === "running" && + runningTimingFilter?.kind === "department" && + runningTimingFilter.key === department.key + ? "已展开,再次点击收起" + : `点击查看全部 ${department.overtimeCount} 个流程` + : undefined, + onSelect: department.overtimeCount + ? () => { + const isActive = + timingDetailMode === "running" && + runningTimingFilter?.kind === "department" && + runningTimingFilter.key === department.key; + setRunningTimingFilter( + isActive + ? null + : { kind: "department", key: department.key }, + ); + setTimingDetailMode(isActive ? null : "running"); + } + : undefined, + }), + )} /> 0 + ? { + title: `临近超期任务:${timingOverview.runningNearOvertimeCount} 个`, + detail: `距该款总标准剩余 ${NEAR_OVERTIME_WINDOW_DAYS} 天内`, + lines: timingOverview.runningNearOvertimeRowIds + .slice(0, 4) + .flatMap((rowId) => { + const row = timingDetails.running.find( + (candidate) => candidate.id === rowId, + ); + if (!row) return []; + const remaining = + (row.standardDays ?? 0) - + (row.totalActualDays ?? 0); + return [ + `${row.openingCode} · ${row.productName}`, + `已用 ${row.totalActualDays ?? "-"} 天,剩余 ${Math.max(0, remaining)} 天`, + ]; + }), + actionLabel: + timingDetailMode === "running" && + runningTimingFilter?.kind === "near-overtime" + ? "已展开,再次点击收起" + : "点击查看临近超期明细", + } + : undefined + } + active={ + timingDetailMode === "running" && + runningTimingFilter?.kind === "near-overtime" + } + onClick={ + timingOverview.runningNearOvertimeCount > 0 + ? () => { + const isActive = + timingDetailMode === "running" && + runningTimingFilter?.kind === "near-overtime"; + setRunningTimingFilter( + isActive ? null : { kind: "near-overtime" }, + ); + setTimingDetailMode(isActive ? null : "running"); + } + : undefined + } /> { + const row = timingDetails.running.find( + (candidate) => + candidate.id === + timingOverview.runningLongestRowId, + ); + return row + ? `${row.openingCode} · ${row.productName}` + : timingOverview.runningLongestTask; + })(), + lines: [ + `当前已用时 ${timingOverview.runningLongestDays} 天 / 标准 ${timingOverview.runningLongestTargetDays ?? "-"} 天`, + ], + actionLabel: + timingDetailMode === "running" && + runningTimingFilter?.kind === "longest" + ? "已展开,再次点击收起" + : "点击查看最长任务明细", + } + : undefined + } + active={ + timingDetailMode === "running" && + runningTimingFilter?.kind === "longest" + } + onClick={ + timingOverview.runningLongestRowId + ? () => { + const isActive = + timingDetailMode === "running" && + runningTimingFilter?.kind === "longest"; + setRunningTimingFilter( + isActive + ? null + : { + kind: "longest", + rowId: timingOverview.runningLongestRowId!, + }, + ); + setTimingDetailMode(isActive ? null : "running"); + } + : undefined + } /> {timingDetailMode === "running" ? ( setTimingDetailMode(null)} + rows={runningTimingDetailRows} + filterLabel={runningTimingFilterLabel} + onClose={() => { + setTimingDetailMode(null); + setRunningTimingFilter(null); + }} onSelectRow={setSelectedRow} /> ) : null}