pcp-tgu: связь планов на таймлайне по basePlanId вместо стрелок/нумерации по времени

Стрелки и бейджи-цифры цепочки строились по ВРЕМЕНИ (соседние по времени
планы КА) и ложно сцепляли два черновика с одной предысторией как «один —
предок другого». Убираем эту логику целиком (buildSequentialLinks,
buildPlanChains, CHAIN_COLORS/chainColor, тип TimelineLink) и показываем
связь только по реальному basePlanId:

- В деталях плана — строка basePlanId (рендерится, если задан).
- При выборе плана на таймлайне подсвечиваем гало два соседних звена по
  basePlanId: предыдущий (planId == basePlanId выбранного) — тёплый янтарь,
  следующий (basePlanId == выбранный) — бирюза. Следующих может быть
  несколько (черновики с одной предысторией) — подсвечиваются все.

Бэкенд: basePlanId/projected проброшены через PlanResponse (pcp-tgu-service)
и TguPlanResponse (pcp-ui-service — прокси /api/tgu-planning/plans, иначе
Jackson срезал бы неизвестные поля). Требует рестарт обоих сервисов.

Заодно: очистка нотиса деталей при смене фокуса (план/ЗРВ) и блок
«Включения» (счётчики съёмок/сбросов) в правой панели.
This commit is contained in:
Дмитрий Соловьев
2026-06-04 11:44:28 +03:00
parent 294b17757d
commit a5a1cdfa64
10 changed files with 150 additions and 96 deletions
@@ -1,7 +1,6 @@
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import type { TimelinePlanSegment, TimelineRange, TimelineRow } from "../../model/timelineTypes";
import type { TimelineRange, TimelineRow } from "../../model/timelineTypes";
import {
buildSequentialLinks,
buildTimeTicks,
buildTimelineLayout,
TIMELINE_AXIS_HEIGHT,
@@ -76,11 +75,11 @@ export function TguTimeline({
);
const segments = layout.segments;
const height = Math.max(260, layout.height);
const segmentByPlanId = useMemo(
() => new Map(segments.map((segment) => [segment.plan.planId, segment])),
[segments]
);
const links = useMemo(() => buildSequentialLinks(rows), [rows]);
// basePlanId выбранного плана — чтобы подсветить его прошлое/будущее звено по РЕАЛЬНОЙ связи
// (а не по времени): предыдущий = план с этим planId, следующий = план с basePlanId == выбранный.
const selectedBasePlanId = selectedPlanId
? segments.find((segment) => segment.plan.planId === selectedPlanId)?.plan.basePlanId ?? undefined
: undefined;
const ticks = useMemo(() => buildTimeTicks(range), [range]);
const nowX =
nowMs >= range.fromMs && nowMs <= range.toMs
@@ -95,12 +94,6 @@ export function TguTimeline({
<div className="tgu-empty">Нет планов в выбранном интервале.</div>
) : (
<svg className="tgu-timeline__svg" viewBox={`0 0 ${fullWidth} ${height}`} style={{ height }} role="img" aria-label="Timeline планов ТГУ">
<defs>
<marker id="tgu-arrow-head" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
<path d="M0 0 L8 4 L0 8 z" fill="#8aa2a6" />
</marker>
</defs>
<rect x="0" y="0" width={fullWidth} height={height} className="tgu-timeline__background" />
<rect x="0" y="0" width={TIMELINE_LABEL_WIDTH} height={height} className="tgu-timeline__labels-background" />
@@ -206,19 +199,21 @@ export function TguTimeline({
);
})}
{links.map((link) => {
const from = segmentByPlanId.get(link.fromPlanId);
const to = segmentByPlanId.get(link.toPlanId);
if (!from || !to) return null;
return <TimelineArrow key={`${link.fromPlanId}-${link.toPlanId}`} from={from} to={to} />;
})}
{segments.map((segment) => {
const y = segment.centerY;
const x1 = TIMELINE_LABEL_WIDTH + segment.x;
const x2 = TIMELINE_LABEL_WIDTH + segment.x + segment.width;
const style = statusStyle(segment.plan.status);
const selected = selectedPlanId === segment.plan.planId;
// Сосед выбранного по цепочке: предыдущий (прошлое) — planId == basePlanId выбранного;
// следующий (будущее) — basePlanId == выбранный. Подсветка по реальной связи, не по времени.
const neighbor = selected
? undefined
: selectedBasePlanId && segment.plan.planId === selectedBasePlanId
? "prev"
: selectedPlanId && segment.plan.basePlanId === selectedPlanId
? "next"
: undefined;
const labelVisible = segment.width > 92;
return (
@@ -228,9 +223,19 @@ export function TguTimeline({
onClick={() => onSelectPlan(segment.plan.planId)}
>
<title>
{segment.plan.planId} · {segment.plan.status} · {formatDateTime(segment.plan.startTime)} -{" "}
{formatDateTime(segment.plan.endTime)}
{segment.plan.planId} · {segment.plan.status}
{neighbor === "prev" ? " · предыдущий в цепочке" : neighbor === "next" ? " · следующий в цепочке" : ""} ·{" "}
{formatDateTime(segment.plan.startTime)} - {formatDateTime(segment.plan.endTime)}
</title>
{neighbor && (
<line
x1={x1}
x2={x2}
y1={y}
y2={y}
className={`tgu-timeline-plan__neighbor tgu-timeline-plan__neighbor--${neighbor}`}
/>
)}
{selected && <line x1={x1} x2={x2} y1={y} y2={y} className="tgu-timeline-plan__selection" />}
<line
x1={x1}
@@ -254,19 +259,6 @@ export function TguTimeline({
);
}
function TimelineArrow({ from, to }: { from: TimelinePlanSegment; to: TimelinePlanSegment }) {
const y1 = from.centerY;
const y2 = to.centerY;
const x1 = TIMELINE_LABEL_WIDTH + from.x + from.width + 4;
const x2 = TIMELINE_LABEL_WIDTH + to.x - 6;
const mid = x1 + Math.max(18, (x2 - x1) / 2);
const path = x2 > x1
? `M ${x1} ${y1} C ${mid} ${y1}, ${mid} ${y2}, ${x2} ${y2}`
: `M ${x1} ${y1} C ${x1 + 20} ${y1 - 18}, ${x2 - 20} ${y2 - 18}, ${x2} ${y2}`;
return <path d={path} className="tgu-timeline__arrow" markerEnd="url(#tgu-arrow-head)" />;
}
function formatTickDate(ms: number): string {
return formatLocalDate(ms);
}