Files
dc-observatio/services/pcp-tgu-ui-service/src/api/slotsApi.ts
T
Дмитрий Соловьев b38a97b9a5 feat(tgu-ui): слоты — панель фильтров, показ всех слотов и статус «недоступен»
- Панель фильтров видимости (статус/КА/ветвь) над строкой выбора: единый
  filteredSlots сужает и таблицу, и карту; пустое состояние с «Сбросить».
- Убран потолок отрисовки карты — общую картину не режем, объём сужает оператор
  фильтрами.
- Карту не перецентрируем после расчёта (был прыжок на произвольный первый слот).
- «Показать все слоты» шлёт all=true и показывает занятые бронью (BUSY); бронь
  их не берёт (исключены из выбора/счётчика, чекбокс disabled).
- Починен мёртвый маппинг статуса: бэкенд шлёт BUSY, фронт ждал OCCUPIED.
- BUSY отображается как «недоступен» (конфликт с бронью), а не «занят» —
  чтобы не путать с самой бронью.
2026-06-15 16:45:28 +03:00

160 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
BookedSlotDTO,
BookedSlotView,
RequestSlotsParams,
SlotBookingRequest,
SlotDTO,
} from '../model/slotTypes'
import { apiFetch } from './apiFetch'
// slots-service через gateway: /api/pcp-slots/** → StripPrefix=2 → slots-service /api/slots/**
const BASE = '/api/pcp-slots/api/slots'
async function readErrorMessage(response: Response, fallback: string): Promise<string> {
try {
const text = await response.text()
if (!text) return `${fallback}: ${response.status}`
try {
const body = JSON.parse(text) as { error?: string; message?: string }
return body.error || body.message || text
} catch {
return text
}
} catch {
return `${fallback}: ${response.status}`
}
}
/**
* Рассчитывает слоты покрытия заявки (slots-service: GET /api/slots/request-cover).
* @param params Заявка, интервал и фильтры (ветвь, признак покрытия, КА, угол Солнца,
* стратегия автоформирования покрытия).
* @param signal Сигнал отмены (AbortController) — прерывает долгий расчёт.
*
* @returns Список слотов-кандидатов с контурами для карты. При заданной coverageStrategy —
* уже отобранный достаточный набор покрытия (преднабор для брони).
*/
export async function fetchRequestSlots(
params: RequestSlotsParams,
signal?: AbortSignal,
): Promise<SlotDTO[]> {
const query = new URLSearchParams()
query.set('requestId', params.requestId)
query.set('timeStart', params.timeStart)
query.set('timeStop', params.timeStop)
if (params.revSign) query.set('revSign', params.revSign)
if (params.cov != null) query.set('cov', String(params.cov))
if (params.sun != null) query.set('sun', String(params.sun))
if (params.coverageStrategy) query.set('coverageStrategy', params.coverageStrategy)
if (params.all != null) query.set('all', String(params.all))
params.satellites?.forEach((id) => query.append('satellites', String(id)))
const response = await apiFetch(`${BASE}/request-cover?${query}`, {
headers: { Accept: 'application/json' },
signal,
})
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Не удалось рассчитать слоты'))
}
return response.json() as Promise<SlotDTO[]>
}
/**
* Бронирует выбранные слоты под заявку (slots-service: POST /api/slots/booking/request).
* @param requestId Идентификатор заявки.
* @param slots Список слотов (КА, номер слота, цикл).
*
* @returns Забронированные слоты.
*/
export async function bookRequestSlots(
requestId: string,
slots: SlotBookingRequest[],
): Promise<BookedSlotDTO[]> {
const response = await apiFetch(`${BASE}/booking/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ requestId, slots }),
})
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Не удалось забронировать слоты'))
}
return response.json() as Promise<BookedSlotDTO[]>
}
/**
* Снимает все брони заявки (slots-service: DELETE /api/slots/booking/request).
* @param requestId Идентификатор заявки.
*
* @returns Сколько броней снято.
*/
export async function cancelRequestBooking(requestId: string): Promise<number> {
const response = await apiFetch(
`${BASE}/booking/request?requestId=${encodeURIComponent(requestId)}`,
{ method: 'DELETE', headers: { Accept: 'application/json' } },
)
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Не удалось снять бронь'))
}
const value = Number(await response.text())
return Number.isFinite(value) ? value : 0
}
/**
* Снимает выбранные брони заявки по списку bookedSlotId (slots-service: DELETE
* /api/slots/booking/batch). Один запрос = один чанк; крупный набор чанкует вызывающий
* (runChunked), как при бронировании. Список id передаётся в query (?ids=1&ids=2), а не в теле —
* тело у DELETE через gateway ненадёжно. requestId обязателен: снятие request-aware, чтобы
* шаренный между заявками слот не сносился у других.
* @param requestId Идентификатор заявки.
* @param bookedSlotIds Идентификаторы броней (bookedSlotId) одного чанка.
*
* @returns Сколько броней снято в этом чанке.
*/
export async function cancelBookedSlots(
requestId: string,
bookedSlotIds: number[],
): Promise<number> {
if (bookedSlotIds.length === 0) return 0
const query = new URLSearchParams()
query.set('requestId', requestId)
bookedSlotIds.forEach((id) => query.append('ids', String(id)))
const response = await apiFetch(`${BASE}/booking/batch?${query}`, {
method: 'DELETE',
headers: { Accept: 'application/json' },
})
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Не удалось снять бронь'))
}
const value = Number(await response.text())
return Number.isFinite(value) ? value : 0
}
/**
* Загружает уже забронированные слоты заявки (slots-service: GET /api/slots/booking/by-request/{id}).
* @param requestId Идентификатор заявки.
*
* @returns Список забронированных слотов с контурами и bookedSlotId (для снятия по id);
* cycle здесь — цикл брони, поэтому slotKey совпадает с ответом POST booking/request.
*/
export async function fetchBookedSlotsByRequest(requestId: string): Promise<BookedSlotView[]> {
const response = await apiFetch(
`${BASE}/booking/by-request/${encodeURIComponent(requestId)}`,
{ headers: { Accept: 'application/json' } },
)
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Не удалось загрузить брони заявки'))
}
return response.json() as Promise<BookedSlotView[]>
}