fix(tgu-ui): гасить «Создать черновик» для стартовавшего плана (replace→422)

Кнопка предлагала replace-черновик для уже стартовавшего плана, хотя бэк
(CreateDraftUseCase.createReplaceDraft) возвращает 422 «окно выдачи заморожено».
Гард canCreateDraftFromPlan теперь требует startInFuture (зеркалит редактор);
для стартовавших eligible-планов вместо кнопки — amber-подсказка про fork-forward.
This commit is contained in:
Дмитрий Соловьев
2026-06-10 15:46:57 +03:00
parent 3765efb4fe
commit 11380fced5
@@ -82,14 +82,20 @@ export function TguPlanDetails({
const duration = formatDuration(plan.endMs - plan.startMs);
// Окно решения закрылось, если с момента старта плана прошло больше grace-периода.
const decisionWindowClosed = Date.now() > plan.startMs + DECISION_GRACE_MINUTES * 60_000;
// Закладку нельзя подтверждать, пока начало плана ещё в будущем: план закладывается на КА
// по факту наступления своего старта, а не заранее.
const layinStartInFuture = plan.startMs > Date.now();
// Начало плана ещё в будущем. Нужно и для закладки (план закладывается по факту старта,
// не заранее), и для replace-черновика (после старта окно выдачи заморожено — бэк вернёт 422).
const startInFuture = plan.startMs > Date.now();
// «Создать черновик на основе плана» (replace, окно 1:1, все включения) доступно для
// персистентных правимых баз. DRAFT уже имеет свои действия; PLANNED — виртуальная проекция,
// не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован, только fork-forward).
const canCreateDraftFromPlan =
// персистентных правимых баз, у которых старт ещё не наступил. DRAFT уже имеет свои действия;
// PLANNED — виртуальная проекция, не база; LAID_IN — replace 1:1 запрещён (слот зафиксирован);
// стартовавший план — окно выдачи заморожено (CreateDraftUseCase.createReplaceDraft → 422),
// продолжение возможно только fork-forward (отдельный поток). Гард зеркалит редактор, чтобы
// не предлагать кнопку, которая заведомо упадёт «Unprocessable Content».
const draftableStatus =
plan.status !== "DRAFT" && plan.status !== "PLANNED" && plan.status !== "LAID_IN";
const canCreateDraftFromPlan = draftableStatus && startInFuture;
// Статус позволял бы replace-черновик, но план уже стартовал — объясняем, почему кнопки нет.
const draftBlockedByStart = draftableStatus && !startInFuture;
const reject = () => {
const reason = window.prompt("Reason для REJECTED", "UI_TEST_REJECTED");
@@ -229,15 +235,23 @@ export function TguPlanDetails({
</div>
)}
{draftBlockedByStart && (
<div className="tgu-alert tgu-alert--warning">
Черновик-копию (replace, то же окно) можно создать только до старта плана окно
выдачи уже заморожено. Продолжение цепочки на будущую ЗРВ (fork-forward) выполняется
отдельно.
</div>
)}
{plan.status === "AWAITING_LAYIN" && onConfirmLayin && (
<div className="tgu-details__test-block">
<div className="tgu-details__test-label">ТЕСТОВЫЙ ФУНКЦИОНАЛ</div>
<button
className="tgu-button tgu-button--accept"
type="button"
disabled={decisionInFlight || layinConflict || layinStartInFuture}
disabled={decisionInFlight || layinConflict || startInFuture}
title={
layinStartInFuture
startInFuture
? "Начало плана ещё не наступило — подтвердить закладку можно только после его старта"
: layinConflict
? "По КА уже заложен план со стартом в пределах ±10 минут — закладка недоступна"
@@ -247,12 +261,12 @@ export function TguPlanDetails({
>
Подтвердить закладку
</button>
{layinStartInFuture && (
{startInFuture && (
<div className="tgu-details__test-hint">
Начало плана ещё в будущем подтвердить закладку можно только после наступления его старта.
</div>
)}
{!layinStartInFuture && layinConflict && (
{!startInFuture && layinConflict && (
<div className="tgu-details__test-hint">
По КА уже есть заложенный план со стартом в пределах ±10 минут подтвердить нельзя.
</div>