b38a97b9a5
- Панель фильтров видимости (статус/КА/ветвь) над строкой выбора: единый filteredSlots сужает и таблицу, и карту; пустое состояние с «Сбросить». - Убран потолок отрисовки карты — общую картину не режем, объём сужает оператор фильтрами. - Карту не перецентрируем после расчёта (был прыжок на произвольный первый слот). - «Показать все слоты» шлёт all=true и показывает занятые бронью (BUSY); бронь их не берёт (исключены из выбора/счётчика, чекбокс disabled). - Починен мёртвый маппинг статуса: бэкенд шлёт BUSY, фронт ждал OCCUPIED. - BUSY отображается как «недоступен» (конфликт с бронью), а не «занят» — чтобы не путать с самой бронью.
160 lines
6.4 KiB
TypeScript
160 lines
6.4 KiB
TypeScript
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[]>
|
||
}
|