feat(tgu-ui): вкладка «Слоты» — расчёт покрытия и бронирование слотов заявки
- новая вкладка: заявки интервала на карте, выбор заявки кликом/инспектором, расчёт слотов покрытия, бронирование и снятие брони - выбор заявки без выпадающего списка (не масштабируется на ~10к): все заявки на карте, панель справа с кнопкой «Выбрать», имя выбранной заявки в тулбаре, кнопка «Сброс», фильтр заявок по интервалу - бронь отмечаем по ответу POST booking/request (by-request отдаёт слот в чужом cycle-фрейме — ключ не совпадал, бейдж «бронь» не появлялся) - подсветка и автоскролл активной строки по клику на слот (ручной scrollBy) - меню выбора при плотном наложении слотов (SlotPickMenu); контур заявки pickable:false, чтобы не «съедать» меню; панель не закрывается при выборе - фильтр отображения по суткам (SlotDaySelector) - gateway: маршрут slots (/api/pcp-slots/**) - карточка полосы обзора (swath): КА и виток в инспекторе
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import type {
|
||||
BookedSlotDTO,
|
||||
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 Заявка, интервал и фильтры (ветвь, признак покрытия, КА, угол Солнца).
|
||||
*
|
||||
* @returns Список слотов-кандидатов с контурами для карты.
|
||||
*/
|
||||
export async function fetchRequestSlots(params: RequestSlotsParams): 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))
|
||||
params.satellites?.forEach((id) => query.append('satellites', String(id)))
|
||||
|
||||
const response = await apiFetch(`${BASE}/request-cover?${query}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает уже забронированные слоты заявки (slots-service: GET /api/slots/booking/by-request/{id}).
|
||||
* @param requestId Идентификатор заявки.
|
||||
*
|
||||
* @returns Список забронированных слотов с контурами.
|
||||
*/
|
||||
export async function fetchBookedSlotsByRequest(requestId: string): Promise<SlotDTO[]> {
|
||||
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<SlotDTO[]>
|
||||
}
|
||||
Reference in New Issue
Block a user