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 { 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 { return postJson( `${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 { return postJson( `${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 { 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(url: string, body: unknown, fallbackMessage: string): Promise { 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; 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; }