feat(supply-chain): add running timing department drilldown
This commit is contained in:
parent
d2046589e8
commit
244950180e
@ -123,9 +123,12 @@ type TimingOverview = {
|
|||||||
runningCount: number;
|
runningCount: number;
|
||||||
runningOvertimeCount: number;
|
runningOvertimeCount: number;
|
||||||
runningNearOvertimeCount: number;
|
runningNearOvertimeCount: number;
|
||||||
|
runningDepartments: CompletedDepartmentTimingSummary[];
|
||||||
|
runningNearOvertimeRowIds: string[];
|
||||||
runningLongestDays: number;
|
runningLongestDays: number;
|
||||||
runningLongestTask: string;
|
runningLongestTask: string;
|
||||||
runningLongestTargetDays: number | null;
|
runningLongestTargetDays: number | null;
|
||||||
|
runningLongestRowId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CompletedTimingDetailFilter =
|
type CompletedTimingDetailFilter =
|
||||||
@ -139,6 +142,20 @@ type CompletedTimingDetailFilter =
|
|||||||
}
|
}
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
|
type RunningTimingDetailFilter =
|
||||||
|
| {
|
||||||
|
kind: "department";
|
||||||
|
key: CompletedDepartmentTimingSummary["key"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "near-overtime";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: "longest";
|
||||||
|
rowId: string;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
|
|
||||||
type BaseAnalysisDatum = (typeof ANALYSIS_DATA)[Exclude<
|
type BaseAnalysisDatum = (typeof ANALYSIS_DATA)[Exclude<
|
||||||
AnalysisView,
|
AnalysisView,
|
||||||
"overview"
|
"overview"
|
||||||
@ -1377,9 +1394,15 @@ function buildTimingOverview(
|
|||||||
runningCount: runningRows.length,
|
runningCount: runningRows.length,
|
||||||
runningOvertimeCount: runningOvertimeRows.length,
|
runningOvertimeCount: runningOvertimeRows.length,
|
||||||
runningNearOvertimeCount: runningNearOvertimeRows.length,
|
runningNearOvertimeCount: runningNearOvertimeRows.length,
|
||||||
|
runningDepartments: summarizeCompletedDepartmentTimings(
|
||||||
|
runningRows,
|
||||||
|
runningOvertimeRows,
|
||||||
|
),
|
||||||
|
runningNearOvertimeRowIds: runningNearOvertimeRows.map((row) => row.id),
|
||||||
runningLongestDays: longestRunningRow?.totalActualDays ?? 0,
|
runningLongestDays: longestRunningRow?.totalActualDays ?? 0,
|
||||||
runningLongestTask: longestRunningRow?.openingCode ?? "暂无进行中任务",
|
runningLongestTask: longestRunningRow?.openingCode ?? "暂无进行中任务",
|
||||||
runningLongestTargetDays: longestRunningRow?.standardDays ?? null,
|
runningLongestTargetDays: longestRunningRow?.standardDays ?? null,
|
||||||
|
runningLongestRowId: longestRunningRow?.id ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2958,6 +2981,8 @@ export default function NewProductManagementDashboard({
|
|||||||
>(null);
|
>(null);
|
||||||
const [completedTimingFilter, setCompletedTimingFilter] =
|
const [completedTimingFilter, setCompletedTimingFilter] =
|
||||||
useState<CompletedTimingDetailFilter>(null);
|
useState<CompletedTimingDetailFilter>(null);
|
||||||
|
const [runningTimingFilter, setRunningTimingFilter] =
|
||||||
|
useState<RunningTimingDetailFilter>(null);
|
||||||
const periodRange = useMemo(
|
const periodRange = useMemo(
|
||||||
() => getPeriodRange(period, anchorDate),
|
() => getPeriodRange(period, anchorDate),
|
||||||
[anchorDate, period],
|
[anchorDate, period],
|
||||||
@ -3050,6 +3075,44 @@ export default function NewProductManagementDashboard({
|
|||||||
: completedTimingFilter?.kind === "max-overtime"
|
: completedTimingFilter?.kind === "max-overtime"
|
||||||
? "最大周期超期任务"
|
? "最大周期超期任务"
|
||||||
: undefined;
|
: 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(
|
const stageData: StageDatum[] = useMemo(
|
||||||
() => (dashboard && liveItems ? buildLiveStageData(liveItems) : STAGE_DATA),
|
() => (dashboard && liveItems ? buildLiveStageData(liveItems) : STAGE_DATA),
|
||||||
[dashboard, liveItems],
|
[dashboard, liveItems],
|
||||||
@ -3264,6 +3327,7 @@ export default function NewProductManagementDashboard({
|
|||||||
aria-expanded={timingDetailMode === "completed"}
|
aria-expanded={timingDetailMode === "completed"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCompletedTimingFilter(null);
|
setCompletedTimingFilter(null);
|
||||||
|
setRunningTimingFilter(null);
|
||||||
setTimingDetailMode((current) =>
|
setTimingDetailMode((current) =>
|
||||||
current === "completed" ? null : "completed",
|
current === "completed" ? null : "completed",
|
||||||
);
|
);
|
||||||
@ -3506,7 +3570,7 @@ export default function NewProductManagementDashboard({
|
|||||||
tone="neutral"
|
tone="neutral"
|
||||||
/>
|
/>
|
||||||
<EfficiencyCard
|
<EfficiencyCard
|
||||||
label="已超时任务"
|
label="周期超期任务"
|
||||||
value={timingOverview.runningOvertimeCount}
|
value={timingOverview.runningOvertimeCount}
|
||||||
suffix="个"
|
suffix="个"
|
||||||
benchmark="当前已用时超过该款总标准"
|
benchmark="当前已用时超过该款总标准"
|
||||||
@ -3516,6 +3580,60 @@ export default function NewProductManagementDashboard({
|
|||||||
: "暂无总周期超时"
|
: "暂无总周期超时"
|
||||||
}
|
}
|
||||||
tone={timingOverview.runningOvertimeCount ? "danger" : "good"}
|
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
|
<EfficiencyCard
|
||||||
label="临近超时数"
|
label="临近超时数"
|
||||||
@ -3530,6 +3648,51 @@ export default function NewProductManagementDashboard({
|
|||||||
tone={
|
tone={
|
||||||
timingOverview.runningNearOvertimeCount ? "warning" : "good"
|
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
|
<EfficiencyCard
|
||||||
label="最长已用时"
|
label="最长已用时"
|
||||||
@ -3550,13 +3713,64 @@ export default function NewProductManagementDashboard({
|
|||||||
? "danger"
|
? "danger"
|
||||||
: "warning"
|
: "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>
|
</div>
|
||||||
{timingDetailMode === "running" ? (
|
{timingDetailMode === "running" ? (
|
||||||
<TimingDetailTable
|
<TimingDetailTable
|
||||||
mode="running"
|
mode="running"
|
||||||
rows={timingDetails.running}
|
rows={runningTimingDetailRows}
|
||||||
onClose={() => setTimingDetailMode(null)}
|
filterLabel={runningTimingFilterLabel}
|
||||||
|
onClose={() => {
|
||||||
|
setTimingDetailMode(null);
|
||||||
|
setRunningTimingFilter(null);
|
||||||
|
}}
|
||||||
onSelectRow={setSelectedRow}
|
onSelectRow={setSelectedRow}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user