feat(supply-chain): add running timing department drilldown

This commit is contained in:
jingchuan 2026-08-19 09:16:52 +08:00
parent d2046589e8
commit 244950180e

View File

@ -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<CompletedTimingDetailFilter>(null);
const [runningTimingFilter, setRunningTimingFilter] =
useState<RunningTimingDetailFilter>(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"
/>
<EfficiencyCard
label="已超时任务"
label="周期超期任务"
value={timingOverview.runningOvertimeCount}
suffix="个"
benchmark="当前已用时超过该款总标准"
@ -3516,6 +3580,60 @@ export default function NewProductManagementDashboard({
: "暂无总周期超时"
}
tone={timingOverview.runningOvertimeCount ? "danger" : "good"}
departmentItems={timingOverview.runningDepartments.map(
(department) => ({
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,
}),
)}
/>
<EfficiencyCard
label="临近超时数"
@ -3530,6 +3648,51 @@ export default function NewProductManagementDashboard({
tone={
timingOverview.runningNearOvertimeCount ? "warning" : "good"
}
hoverDetail={
timingOverview.runningNearOvertimeCount > 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
}
/>
<EfficiencyCard
label="最长已用时"
@ -3550,13 +3713,64 @@ export default function NewProductManagementDashboard({
? "danger"
: "warning"
}
hoverDetail={
timingOverview.runningLongestRowId
? {
title: `最长已用时:${timingOverview.runningLongestDays} 天`,
detail: (() => {
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
}
/>
</div>
{timingDetailMode === "running" ? (
<TimingDetailTable
mode="running"
rows={timingDetails.running}
onClose={() => setTimingDetailMode(null)}
rows={runningTimingDetailRows}
filterLabel={runningTimingFilterLabel}
onClose={() => {
setTimingDetailMode(null);
setRunningTimingFilter(null);
}}
onSelectRow={setSelectedRow}
/>
) : null}