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:
@@ -11,8 +11,16 @@ import { formatLocalDateTime, parseLocal } from "../../utils/localTime";
|
|||||||
// config-repo/pcp-tgu-service.yaml → tgu.planning.waiting-decision-grace-minutes.
|
// config-repo/pcp-tgu-service.yaml → tgu.planning.waiting-decision-grace-minutes.
|
||||||
const DECISION_GRACE_MINUTES = 30;
|
const DECISION_GRACE_MINUTES = 30;
|
||||||
|
|
||||||
|
/** Состав включений плана: сколько съёмок и сбросов. undefined — ещё грузится / неизвестно. */
|
||||||
|
export type PlanModeCounts = {
|
||||||
|
survey: number;
|
||||||
|
drop: number;
|
||||||
|
};
|
||||||
|
|
||||||
type TguPlanDetailsProps = {
|
type TguPlanDetailsProps = {
|
||||||
plan?: TguPlanUi;
|
plan?: TguPlanUi;
|
||||||
|
/** Состав включений плана для блока «Включения» (съёмки/сбросы). */
|
||||||
|
modeCounts?: PlanModeCounts;
|
||||||
/** Цепочка КА на паузе после REJECTED — показываем подсказку про ручной исправленный план. */
|
/** Цепочка КА на паузе после REJECTED — показываем подсказку про ручной исправленный план. */
|
||||||
chainPaused?: boolean;
|
chainPaused?: boolean;
|
||||||
decisionInFlight: boolean;
|
decisionInFlight: boolean;
|
||||||
@@ -29,6 +37,7 @@ type TguPlanDetailsProps = {
|
|||||||
|
|
||||||
export function TguPlanDetails({
|
export function TguPlanDetails({
|
||||||
plan,
|
plan,
|
||||||
|
modeCounts,
|
||||||
chainPaused,
|
chainPaused,
|
||||||
decisionInFlight,
|
decisionInFlight,
|
||||||
notice,
|
notice,
|
||||||
@@ -95,6 +104,13 @@ export function TguPlanDetails({
|
|||||||
<Detail label="duration" value={duration} mono />
|
<Detail label="duration" value={duration} mono />
|
||||||
<Detail label="kppId" value={plan.kppId} mono />
|
<Detail label="kppId" value={plan.kppId} mono />
|
||||||
<Detail label="status" value={plan.status} mono />
|
<Detail label="status" value={plan.status} mono />
|
||||||
|
{plan.basePlanId && <Detail label="basePlanId" value={plan.basePlanId} mono />}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="tgu-details__eyebrow">Включения</div>
|
||||||
|
<dl className="tgu-details-grid">
|
||||||
|
<Detail label="съёмок" value={modeCounts ? String(modeCounts.survey) : "…"} mono />
|
||||||
|
<Detail label="сбросов" value={modeCounts ? String(modeCounts.drop) : "…"} mono />
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{plan.status === "DRAFT" && (
|
{plan.status === "DRAFT" && (
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import {
|
|||||||
promoteDraft,
|
promoteDraft,
|
||||||
type ZrvWindowResponse
|
type ZrvWindowResponse
|
||||||
} from "../../api/tguPlanApi";
|
} from "../../api/tguPlanApi";
|
||||||
|
import { fetchPlanModes } from "../tgu-editor/editorApi";
|
||||||
import { CreateChainPlanDialog } from "./CreateChainPlanDialog";
|
import { CreateChainPlanDialog } from "./CreateChainPlanDialog";
|
||||||
import { TguPlanDetails } from "./TguPlanDetails";
|
import { TguPlanDetails, type PlanModeCounts } from "./TguPlanDetails";
|
||||||
import { TguPlanningLayout } from "./TguPlanningLayout";
|
import { TguPlanningLayout } from "./TguPlanningLayout";
|
||||||
import { TguSidebar } from "./TguSidebar";
|
import { TguSidebar } from "./TguSidebar";
|
||||||
import { TguTimeline, type ZrvWindow } from "./TguTimeline";
|
import { TguTimeline, type ZrvWindow } from "./TguTimeline";
|
||||||
@@ -208,6 +209,32 @@ export function TguPlanningPage() {
|
|||||||
return { ...plan, platform };
|
return { ...plan, platform };
|
||||||
}, [allPlans, platforms, selectedPlanId]);
|
}, [allPlans, platforms, selectedPlanId]);
|
||||||
|
|
||||||
|
// Состав включений выбранного плана (счётчики съёмок/сбросов) для правой панели.
|
||||||
|
// planId == mission_id, поэтому грузим режимы плана тем же эндпоинтом, что и редактор.
|
||||||
|
const [planModeCounts, setPlanModeCounts] = useState<PlanModeCounts>();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedPlanId) {
|
||||||
|
setPlanModeCounts(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
setPlanModeCounts(undefined);
|
||||||
|
void fetchPlanModes(selectedPlanId)
|
||||||
|
.then((modes) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setPlanModeCounts({
|
||||||
|
survey: modes.filter((mode) => mode.type === "SURVEY").length,
|
||||||
|
drop: modes.filter((mode) => mode.type === "DROP").length
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setPlanModeCounts(undefined);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [selectedPlanId]);
|
||||||
|
|
||||||
const applyRange = () => {
|
const applyRange = () => {
|
||||||
setDecisionNotice(undefined);
|
setDecisionNotice(undefined);
|
||||||
if (invalidRange) {
|
if (invalidRange) {
|
||||||
@@ -351,6 +378,8 @@ export function TguPlanningPage() {
|
|||||||
selectedPlanId={selectedPlanId}
|
selectedPlanId={selectedPlanId}
|
||||||
onSelectPlan={(planId) => {
|
onSelectPlan={(planId) => {
|
||||||
setZrvSelection(undefined);
|
setZrvSelection(undefined);
|
||||||
|
setDecisionNotice(undefined);
|
||||||
|
setDecisionError(undefined);
|
||||||
setSelectedPlanId(planId);
|
setSelectedPlanId(planId);
|
||||||
const plan = allPlans.find((item) => item.planId === planId);
|
const plan = allPlans.find((item) => item.planId === planId);
|
||||||
if (plan) {
|
if (plan) {
|
||||||
@@ -359,6 +388,8 @@ export function TguPlanningPage() {
|
|||||||
}}
|
}}
|
||||||
onSelectZrv={(windows, index) => {
|
onSelectZrv={(windows, index) => {
|
||||||
setSelectedPlanId(undefined);
|
setSelectedPlanId(undefined);
|
||||||
|
setDecisionNotice(undefined);
|
||||||
|
setDecisionError(undefined);
|
||||||
setZrvSelection({ windows, index });
|
setZrvSelection({ windows, index });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -373,6 +404,7 @@ export function TguPlanningPage() {
|
|||||||
) : (
|
) : (
|
||||||
<TguPlanDetails
|
<TguPlanDetails
|
||||||
plan={selectedPlan}
|
plan={selectedPlan}
|
||||||
|
modeCounts={planModeCounts}
|
||||||
chainPaused={selectedPlan ? paused.has(selectedPlan.spacecraftId) : false}
|
chainPaused={selectedPlan ? paused.has(selectedPlan.spacecraftId) : false}
|
||||||
decisionInFlight={decisionInFlight}
|
decisionInFlight={decisionInFlight}
|
||||||
notice={decisionNotice}
|
notice={decisionNotice}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { TimelinePlanSegment, TimelineRange, TimelineRow } from "../../model/timelineTypes";
|
import type { TimelineRange, TimelineRow } from "../../model/timelineTypes";
|
||||||
import {
|
import {
|
||||||
buildSequentialLinks,
|
|
||||||
buildTimeTicks,
|
buildTimeTicks,
|
||||||
buildTimelineLayout,
|
buildTimelineLayout,
|
||||||
TIMELINE_AXIS_HEIGHT,
|
TIMELINE_AXIS_HEIGHT,
|
||||||
@@ -76,11 +75,11 @@ export function TguTimeline({
|
|||||||
);
|
);
|
||||||
const segments = layout.segments;
|
const segments = layout.segments;
|
||||||
const height = Math.max(260, layout.height);
|
const height = Math.max(260, layout.height);
|
||||||
const segmentByPlanId = useMemo(
|
// basePlanId выбранного плана — чтобы подсветить его прошлое/будущее звено по РЕАЛЬНОЙ связи
|
||||||
() => new Map(segments.map((segment) => [segment.plan.planId, segment])),
|
// (а не по времени): предыдущий = план с этим planId, следующий = план с basePlanId == выбранный.
|
||||||
[segments]
|
const selectedBasePlanId = selectedPlanId
|
||||||
);
|
? segments.find((segment) => segment.plan.planId === selectedPlanId)?.plan.basePlanId ?? undefined
|
||||||
const links = useMemo(() => buildSequentialLinks(rows), [rows]);
|
: undefined;
|
||||||
const ticks = useMemo(() => buildTimeTicks(range), [range]);
|
const ticks = useMemo(() => buildTimeTicks(range), [range]);
|
||||||
const nowX =
|
const nowX =
|
||||||
nowMs >= range.fromMs && nowMs <= range.toMs
|
nowMs >= range.fromMs && nowMs <= range.toMs
|
||||||
@@ -95,12 +94,6 @@ export function TguTimeline({
|
|||||||
<div className="tgu-empty">Нет планов в выбранном интервале.</div>
|
<div className="tgu-empty">Нет планов в выбранном интервале.</div>
|
||||||
) : (
|
) : (
|
||||||
<svg className="tgu-timeline__svg" viewBox={`0 0 ${fullWidth} ${height}`} style={{ height }} role="img" aria-label="Timeline планов ТГУ">
|
<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={fullWidth} height={height} className="tgu-timeline__background" />
|
||||||
<rect x="0" y="0" width={TIMELINE_LABEL_WIDTH} height={height} className="tgu-timeline__labels-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) => {
|
{segments.map((segment) => {
|
||||||
const y = segment.centerY;
|
const y = segment.centerY;
|
||||||
const x1 = TIMELINE_LABEL_WIDTH + segment.x;
|
const x1 = TIMELINE_LABEL_WIDTH + segment.x;
|
||||||
const x2 = TIMELINE_LABEL_WIDTH + segment.x + segment.width;
|
const x2 = TIMELINE_LABEL_WIDTH + segment.x + segment.width;
|
||||||
const style = statusStyle(segment.plan.status);
|
const style = statusStyle(segment.plan.status);
|
||||||
const selected = selectedPlanId === segment.plan.planId;
|
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;
|
const labelVisible = segment.width > 92;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,9 +223,19 @@ export function TguTimeline({
|
|||||||
onClick={() => onSelectPlan(segment.plan.planId)}
|
onClick={() => onSelectPlan(segment.plan.planId)}
|
||||||
>
|
>
|
||||||
<title>
|
<title>
|
||||||
{segment.plan.planId} · {segment.plan.status} · {formatDateTime(segment.plan.startTime)} -{" "}
|
{segment.plan.planId} · {segment.plan.status}
|
||||||
{formatDateTime(segment.plan.endTime)}
|
{neighbor === "prev" ? " · предыдущий в цепочке" : neighbor === "next" ? " · следующий в цепочке" : ""} ·{" "}
|
||||||
|
{formatDateTime(segment.plan.startTime)} - {formatDateTime(segment.plan.endTime)}
|
||||||
</title>
|
</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" />}
|
{selected && <line x1={x1} x2={x2} y1={y} y2={y} className="tgu-timeline-plan__selection" />}
|
||||||
<line
|
<line
|
||||||
x1={x1}
|
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 {
|
function formatTickDate(ms: number): string {
|
||||||
return formatLocalDate(ms);
|
return formatLocalDate(ms);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { TguPlanUi, TimelineRow } from "../../model/timelineTypes";
|
import type { TguPlanUi, TimelineRow } from "../../model/timelineTypes";
|
||||||
import { buildSequentialLinks, buildTimelineLayout, clipPlanToRange, timeToX } from "./tguTimelineLayout";
|
import { buildTimelineLayout, clipPlanToRange, timeToX } from "./tguTimelineLayout";
|
||||||
import { parseLocal } from "../../utils/localTime";
|
import { parseLocal } from "../../utils/localTime";
|
||||||
|
|
||||||
const range = {
|
const range = {
|
||||||
@@ -8,7 +8,13 @@ const range = {
|
|||||||
toMs: parseLocal("2026-05-30T00:00:00")
|
toMs: parseLocal("2026-05-30T00:00:00")
|
||||||
};
|
};
|
||||||
|
|
||||||
function plan(id: string, startTime: string, endTime: string, spacecraftId = "56756"): TguPlanUi {
|
function plan(
|
||||||
|
id: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string,
|
||||||
|
spacecraftId = "56756",
|
||||||
|
extra: Partial<TguPlanUi> = {}
|
||||||
|
): TguPlanUi {
|
||||||
return {
|
return {
|
||||||
planId: id,
|
planId: id,
|
||||||
spacecraftId,
|
spacecraftId,
|
||||||
@@ -17,7 +23,8 @@ function plan(id: string, startTime: string, endTime: string, spacecraftId = "56
|
|||||||
kppId: "KPP-1",
|
kppId: "KPP-1",
|
||||||
status: "PLANNED",
|
status: "PLANNED",
|
||||||
startMs: parseLocal(startTime),
|
startMs: parseLocal(startTime),
|
||||||
endMs: parseLocal(endTime)
|
endMs: parseLocal(endTime),
|
||||||
|
...extra
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,28 +42,6 @@ describe("tguTimelineLayout", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds links only between sequential plans of the same spacecraft", () => {
|
|
||||||
const rows: TimelineRow[] = [
|
|
||||||
{
|
|
||||||
spacecraftId: "56756",
|
|
||||||
plans: [
|
|
||||||
plan("p2", "2026-05-29T04:00:00", "2026-05-29T05:00:00"),
|
|
||||||
plan("p1", "2026-05-29T01:00:00", "2026-05-29T02:00:00")
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
spacecraftId: "777",
|
|
||||||
plans: [
|
|
||||||
plan("p3", "2026-05-29T01:00:00", "2026-05-29T02:00:00", "777")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(buildSequentialLinks(rows)).toEqual([
|
|
||||||
{ spacecraftId: "56756", fromPlanId: "p1", toPlanId: "p2" }
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps non-overlapping plans on a single lane", () => {
|
it("keeps non-overlapping plans on a single lane", () => {
|
||||||
const rows: TimelineRow[] = [
|
const rows: TimelineRow[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
TguPlanUi,
|
TguPlanUi,
|
||||||
TimelineLink,
|
|
||||||
TimelinePlanSegment,
|
TimelinePlanSegment,
|
||||||
TimelineRange,
|
TimelineRange,
|
||||||
TimelineRow,
|
TimelineRow,
|
||||||
@@ -118,23 +117,6 @@ export function buildTimelineLayout(
|
|||||||
return { rowLayouts, segments, height: cursorY + TIMELINE_BOTTOM_PADDING };
|
return { rowLayouts, segments, height: cursorY + TIMELINE_BOTTOM_PADDING };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSequentialLinks(rows: TimelineRow[]): TimelineLink[] {
|
|
||||||
const links: TimelineLink[] = [];
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const sorted = [...row.plans].sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs);
|
|
||||||
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
||||||
links.push({
|
|
||||||
spacecraftId: row.spacecraftId,
|
|
||||||
fromPlanId: sorted[index].planId,
|
|
||||||
toPlanId: sorted[index + 1].planId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return links;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTimeTicks(range: TimelineRange, targetCount = 8): number[] {
|
export function buildTimeTicks(range: TimelineRange, targetCount = 8): number[] {
|
||||||
const span = range.toMs - range.fromMs;
|
const span = range.toMs - range.fromMs;
|
||||||
const roughStep = span / targetCount;
|
const roughStep = span / targetCount;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ export type TguPlan = {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
kppId: string;
|
kppId: string;
|
||||||
status: TguPlanStatus;
|
status: TguPlanStatus;
|
||||||
|
/**
|
||||||
|
* Предшественник плана в цепочке. `null`/отсутствует — у плана нет предыстории (старт новой
|
||||||
|
* цепочки «без истории» или самый первый план): на таймлайне в него не ведут стрелку.
|
||||||
|
*/
|
||||||
|
basePlanId?: string | null;
|
||||||
|
/** `true` — вычисленная проекция будущего (не persisted); цепочку продолжает, но стартом не является. */
|
||||||
|
projected?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TguPlatform = {
|
export type TguPlatform = {
|
||||||
|
|||||||
@@ -54,12 +54,6 @@ export type TimelineRowLayout = {
|
|||||||
zrvBandY: number;
|
zrvBandY: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TimelineLink = {
|
|
||||||
spacecraftId: string;
|
|
||||||
fromPlanId: string;
|
|
||||||
toPlanId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TimelineStatusStyle = {
|
export type TimelineStatusStyle = {
|
||||||
status: TguPlanStatus;
|
status: TguPlanStatus;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -422,13 +422,6 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tgu-timeline__arrow {
|
|
||||||
fill: none;
|
|
||||||
stroke: #8aa2a6;
|
|
||||||
stroke-width: 1.5;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tgu-timeline-plan {
|
.tgu-timeline-plan {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -450,6 +443,21 @@
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Гало вокруг соседнего по цепочке плана при выборе: прошлое — тёплый янтарь, будущее — бирюза. */
|
||||||
|
.tgu-timeline-plan__neighbor {
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 19;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-timeline-plan__neighbor--prev {
|
||||||
|
stroke: #e0a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tgu-timeline-plan__neighbor--next {
|
||||||
|
stroke: #46b3c4;
|
||||||
|
}
|
||||||
|
|
||||||
.tgu-timeline-plan__label {
|
.tgu-timeline-plan__label {
|
||||||
fill: #101214;
|
fill: #101214;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -916,6 +924,22 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tgu-2d-map__coords {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
background: rgba(8, 16, 15, 0.72);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "SFMono-Regular", Consolas, monospace;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tgu-2d-map__zoom {
|
.tgu-2d-map__zoom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
|
|||||||
+21
-3
@@ -31,7 +31,21 @@ data class PlanResponse(
|
|||||||
val kppId: String,
|
val kppId: String,
|
||||||
|
|
||||||
/** Статус persisted плана в жизненном цикле выдачи. */
|
/** Статус persisted плана в жизненном цикле выдачи. */
|
||||||
val status: PlannedPlanStatus
|
val status: PlannedPlanStatus,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Непосредственный предшественник плана в цепочке (`base_plan_id`). `null` — у плана нет
|
||||||
|
* предыстории: это начало новой цепочки (создан «без истории») либо самый первый план.
|
||||||
|
* Фронт по этому полю решает, рисовать ли входящую стрелку: в старт цепочки она не ведёт.
|
||||||
|
*/
|
||||||
|
val basePlanId: UUID? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `true` — это вычисленная на лету проекция будущего (в БД не записана), а не persisted-план.
|
||||||
|
* У проекции предшественник не хранится, но цепочку она продолжает — фронт связывает её
|
||||||
|
* соседние звенья по времени, поэтому начало цепочки определяется ТОЛЬКО для persisted.
|
||||||
|
*/
|
||||||
|
val projected: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -46,7 +60,9 @@ data class PlanResponse(
|
|||||||
startTime = plan.startTime,
|
startTime = plan.startTime,
|
||||||
endTime = plan.endTime,
|
endTime = plan.endTime,
|
||||||
kppId = plan.kppId,
|
kppId = plan.kppId,
|
||||||
status = plan.status
|
status = plan.status,
|
||||||
|
basePlanId = plan.basePlanId,
|
||||||
|
projected = false
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +78,9 @@ data class PlanResponse(
|
|||||||
startTime = plan.startTime,
|
startTime = plan.startTime,
|
||||||
endTime = plan.endTime,
|
endTime = plan.endTime,
|
||||||
kppId = plan.kppId,
|
kppId = plan.kppId,
|
||||||
status = PlannedPlanStatus.PLANNED
|
status = PlannedPlanStatus.PLANNED,
|
||||||
|
basePlanId = null,
|
||||||
|
projected = true
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun deterministicPlanId(plan: CalculatedPlan): UUID {
|
private fun deterministicPlanId(plan: CalculatedPlan): UUID {
|
||||||
|
|||||||
+5
-1
@@ -26,7 +26,11 @@ data class TguPlanResponse(
|
|||||||
val startTime: LocalDateTime,
|
val startTime: LocalDateTime,
|
||||||
val endTime: LocalDateTime,
|
val endTime: LocalDateTime,
|
||||||
val kppId: String,
|
val kppId: String,
|
||||||
val status: String
|
val status: String,
|
||||||
|
/** Предшественник в цепочке (`null` — старт без предыстории). Проксируется из pcp-tgu-service. */
|
||||||
|
val basePlanId: UUID? = null,
|
||||||
|
/** `true` — проекция будущего (не persisted). Проксируется из pcp-tgu-service. */
|
||||||
|
val projected: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class TguPlanDecision {
|
enum class TguPlanDecision {
|
||||||
|
|||||||
Reference in New Issue
Block a user