057452f19d
Селектор «Целевая ЗРВ» в EditorToolbar: текущий слот (replace, по умолчанию)
либо будущая ЗРВ из проекции pcp-tgu-service (fetchSpacecraftPlans →
GET /api/plans/{spacecraftId}, фильтр futureZrvWindows: PLANNED строго в
будущем, кроме базы — та же проекция, что валидирует бэк). Выбор окна
переводит обзор редактора и заставляет ensureDraft слать mode=fork-forward
(naive-UTC окно + kpp); селектор блокируется после создания DRAFT.
151 lines
6.2 KiB
TypeScript
151 lines
6.2 KiB
TypeScript
import { TguApiError } from "../model/tguTypes";
|
||
|
||
/**
|
||
* Вызовы версионирования/выдачи плана в pcp-tgu-service напрямую через Vite-прокси
|
||
* (`/api/pcp-tgu` → :7011), как и pcp-mission напрямую на :7010. BFF не используется.
|
||
*/
|
||
const BASE = "/api/pcp-tgu";
|
||
|
||
/** Режим наполнения черновика; та же строка уходит в clone миссии. */
|
||
export type DraftMode = "replace" | "fork-forward";
|
||
|
||
export type CreateDraftRequest = {
|
||
mode: DraftMode;
|
||
/** Начало окна целевой ЗРВ (UTC naive, ISO без зоны). Обязательно для fork-forward. */
|
||
windowStart?: string;
|
||
/** Конец окна целевой ЗРВ. Обязательно для fork-forward. */
|
||
windowStop?: string;
|
||
/** КПП целевой ЗРВ. Обязательно для fork-forward. */
|
||
windowKppId?: string;
|
||
};
|
||
|
||
export type CreateDraftResponse = {
|
||
/** Идентификатор созданного черновика (= mission_id клон-миссии). */
|
||
draftPlanId: string;
|
||
};
|
||
|
||
export type PromoteDraftResponse = {
|
||
processInstanceKey: number;
|
||
processDefinitionKey: number;
|
||
bpmnProcessId: string;
|
||
version: number;
|
||
planId: string;
|
||
attemptId: string;
|
||
};
|
||
|
||
/** План КА из проекции pcp-tgu-service (persisted ∪ будущая проекция). Времена — ISO с `Z`. */
|
||
export type TguPlanResponse = {
|
||
planId: string;
|
||
spacecraftId: string;
|
||
startTime: string;
|
||
endTime: string;
|
||
kppId: string;
|
||
status: string;
|
||
};
|
||
|
||
/**
|
||
* Цепочка планов одного КА из pcp-tgu-service (GET /api/plans/{spacecraftId}): persisted планы плюс
|
||
* будущая проекция (PLANNED). Используется fork-forward-пикером для выбора целевой ЗРВ — бэк сверяет
|
||
* выбранное окно ровно с этой же проекцией (assertWindowMatchesProjection).
|
||
*/
|
||
export async function fetchSpacecraftPlans(spacecraftId: string): Promise<TguPlanResponse[]> {
|
||
const url = `${BASE}/api/plans/${encodeURIComponent(spacecraftId)}`;
|
||
let response: Response;
|
||
try {
|
||
response = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
||
} catch {
|
||
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
|
||
}
|
||
const text = await response.text();
|
||
if (!response.ok) {
|
||
throw new TguApiError(extractErrorText(text) || "Не удалось получить планы КА.", "http", response.status);
|
||
}
|
||
return text.trim() ? (JSON.parse(text) as TguPlanResponse[]) : [];
|
||
}
|
||
|
||
/**
|
||
* Создаёт черновик плана (DRAFT) на основе базового плана
|
||
* (pcp-tgu-service, POST /api/plans/{basePlanId}/draft). Триггерит клон миссии; возвращает
|
||
* draftPlanId, в который адресуются правки (Ф3) и который применяется promote (Ф5).
|
||
*/
|
||
export async function createDraft(basePlanId: string, request: CreateDraftRequest): Promise<CreateDraftResponse> {
|
||
return postJson<CreateDraftResponse>(
|
||
`${BASE}/api/plans/${encodeURIComponent(basePlanId)}/draft`,
|
||
request,
|
||
"Не удалось создать черновик плана."
|
||
);
|
||
}
|
||
|
||
/**
|
||
* «Применить» черновик: промоутит DRAFT в живую цепочку и выдаёт его
|
||
* (pcp-tgu-service, POST /api/plans/{draftPlanId}/promote). Под оптимистичным локом базы и
|
||
* гардом точки невозврата: 409 — база уехала / уже идёт выдача; 422 — не DRAFT / слот сместился /
|
||
* заморожено; 404 — черновика нет.
|
||
*/
|
||
export async function promoteDraft(draftPlanId: string): Promise<PromoteDraftResponse> {
|
||
return postJson<PromoteDraftResponse>(
|
||
`${BASE}/api/plans/${encodeURIComponent(draftPlanId)}/promote`,
|
||
undefined,
|
||
"Не удалось применить черновик плана."
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Отказ от черновика (pcp-tgu-service, DELETE /api/plans/{draftPlanId}): удаляет DRAFT и его
|
||
* клон-миссию, снимая блокировку «один открытый черновик на базу». 404 — нет; 422 — не DRAFT.
|
||
*/
|
||
export async function abandonDraft(draftPlanId: string): Promise<void> {
|
||
const url = `${BASE}/api/plans/${encodeURIComponent(draftPlanId)}`;
|
||
let response: Response;
|
||
try {
|
||
response = await fetch(url, { method: "DELETE", headers: { Accept: "application/json" } });
|
||
} catch {
|
||
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
|
||
}
|
||
if (!response.ok) {
|
||
const text = await response.text();
|
||
throw new TguApiError(extractErrorText(text) || "Не удалось отменить черновик плана.", "http", response.status);
|
||
}
|
||
}
|
||
|
||
async function postJson<T>(url: string, body: unknown, fallbackMessage: string): Promise<T> {
|
||
let response: Response;
|
||
try {
|
||
response = await fetch(url, {
|
||
method: "POST",
|
||
headers: body === undefined
|
||
? { Accept: "application/json" }
|
||
: { Accept: "application/json", "Content-Type": "application/json" },
|
||
...(body === undefined ? {} : { body: JSON.stringify(body) })
|
||
});
|
||
} catch {
|
||
throw new TguApiError("Не удалось подключиться к сервису планирования ТГУ.", "network");
|
||
}
|
||
|
||
const text = await response.text();
|
||
if (!response.ok) {
|
||
throw new TguApiError(extractErrorText(text) || fallbackMessage, "http", response.status);
|
||
}
|
||
if (!text.trim()) {
|
||
return undefined as T;
|
||
}
|
||
return JSON.parse(text) as T;
|
||
}
|
||
|
||
function extractErrorText(body: string): string {
|
||
const trimmed = body.trim();
|
||
if (!trimmed) return "";
|
||
try {
|
||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||
for (const field of ["message", "error", "reason", "detail"]) {
|
||
const value = parsed[field];
|
||
if (typeof value === "string" && value.trim()) {
|
||
return value;
|
||
}
|
||
}
|
||
} catch {
|
||
return trimmed;
|
||
}
|
||
return trimmed;
|
||
}
|