feat(supply-chain): improve timing detail drilldown
This commit is contained in:
parent
641a162675
commit
ef6392be81
@ -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({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] font-medium text-[#647176]">{label}</span>
|
||||
{onClick ? (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-[#a3adaf] transition group-hover:translate-x-0.5" />
|
||||
<ChevronRight className="h-3.5 w-3.5 text-[#a3adaf] transition group-hover/card:translate-x-0.5" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
@ -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({
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
aria-pressed={item.active}
|
||||
aria-label={`${item.tooltipTitle}。${item.tooltipDetail}。${item.actionLabel ?? ""}`}
|
||||
onClick={item.onSelect}
|
||||
className={itemClassName}
|
||||
@ -2019,11 +2043,46 @@ function EfficiencyCard({
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{hoverDetail ? (
|
||||
<span
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute bottom-[calc(100%+8px)] right-0 z-50 hidden w-[min(320px,calc(100vw-32px))] rounded-md border border-black/[0.08] bg-[#182225] px-3 py-2.5 text-left text-white shadow-[0_10px_28px_rgba(16,28,31,0.22)] group-hover/card:block group-focus/card:block"
|
||||
>
|
||||
<span className="block text-[11px] font-semibold leading-4">
|
||||
{hoverDetail.title}
|
||||
</span>
|
||||
<span className="mt-1 block text-[10px] font-normal leading-4 text-white/70">
|
||||
{hoverDetail.detail}
|
||||
</span>
|
||||
{hoverDetail.lines?.length ? (
|
||||
<span className="mt-2 block border-t border-white/15 pt-1.5">
|
||||
{hoverDetail.lines.map((line) => (
|
||||
<span
|
||||
key={line}
|
||||
className="block py-0.5 text-[10px] leading-4 text-white/75"
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
{hoverDetail.actionLabel ? (
|
||||
<span className="mt-1.5 block text-[10px] font-semibold text-[#8fd4c2]">
|
||||
{hoverDetail.actionLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
const className = `group flex min-h-[104px] flex-col justify-between rounded-lg border border-l-[3px] border-black/[0.08] bg-white px-4 py-3 text-left ${onClick ? "transition hover:bg-[#fbfcfc] hover:shadow-[0_8px_20px_rgba(30,40,46,0.06)]" : ""} ${toneClass}`;
|
||||
const className = `group/card relative flex min-h-[104px] flex-col justify-between rounded-lg border border-l-[3px] bg-white px-4 py-3 text-left ${active ? "border-[#7aa69f] bg-[#f8fcfb] ring-2 ring-[#245f5a]/15" : "border-black/[0.08]"} ${onClick ? "transition hover:bg-[#fbfcfc] hover:shadow-[0_8px_20px_rgba(30,40,46,0.06)]" : ""} ${toneClass}`;
|
||||
return onClick ? (
|
||||
<button type="button" onClick={onClick} className={className}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
@ -2174,10 +2233,11 @@ function TimingDetailTable({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="inline-flex items-center gap-1 text-[#647176] hover:text-[#182225]"
|
||||
aria-label={`收起${filterLabel ? `当前${filterLabel}` : isCompleted ? "已完成详细时效" : "进行中详细时效"}`}
|
||||
className="inline-flex h-7 items-center gap-1 rounded-md border border-black/[0.08] bg-white px-2 text-[10px] font-medium text-[#647176] hover:bg-[#f3f6f6] hover:text-[#182225]"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
{isCompleted ? "收起已完成详细时效" : "收起进行中详细时效"}
|
||||
<X className="h-3.5 w-3.5" />
|
||||
{filterLabel ? "收起当前明细" : "收起详细时效"}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
@ -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<CompletedTimingDetailFilter>(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<TimingDetailRow | null>(
|
||||
(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
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{timingDetailMode === "completed" ? (
|
||||
<TimingDetailTable
|
||||
mode="completed"
|
||||
rows={completedTimingDetailRows}
|
||||
filterLabel={
|
||||
selectedCompletedDepartment
|
||||
? `${selectedCompletedDepartment.label}超时流程`
|
||||
: undefined
|
||||
}
|
||||
filterLabel={completedTimingFilterLabel}
|
||||
onClose={() => {
|
||||
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",
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user