import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client' import type { ApplyResultDto, BumperSettings, BumperTextKind, BumperTrigger, ChannelDto, ChannelSummaryDto, CopyTemplateResultDto, CreatedIdResponse, EntryTraceDto, JunctionAmountMode, JunctionConditions, JunctionElementKind, JunctionTemplateDto, LayerApplicability, PlanningRules, ScheduleDiffDto, ScheduleEntryDto, SchedulePreviewDto, ScheduleTemplateDto, SlotDto, TemplateIssueDto, ViewerSettings, } from '@/shared/api/types' export function listChannels() { return apiRequest('/admin/channels') } export function getChannel(id: string) { return apiRequest(`/admin/channels/${id}`) } export function createChannel(body: { name: string; slug: string }) { return apiRequest('/admin/channels', { method: 'POST', body }) } type ChannelSettingsBody = { name: string isEnabled: boolean bumpersEnabled: boolean bumper: BumperSettings fillerAssetId: string | null } export function updateChannelSettings(id: string, body: ChannelSettingsBody) { return apiRequest(`/admin/channels/${id}/settings`, { method: 'PUT', body }) } /** Оверлеи и аналоговый фильтр канала — как он выглядит у зрителя. */ export function updateViewerSettings(id: string, body: ViewerSettings) { return apiRequest(`/admin/channels/${id}/viewer`, { method: 'PUT', body }) } /** Номер канала и его время: смещение от UTC и начало вещательных суток. */ export function updateChannelTime( id: string, body: { number: number | null; utcOffsetMinutes: number; dayStartTime: string }, ) { return apiRequest(`/admin/channels/${id}/time`, { method: 'PUT', body }) } // ── Сетка канала ────────────────────────────────────────────────────────── export function getChannelTemplate(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template`) } /** Заводит каналу сетку, если её нет: у каналов из старой ротации шаблона может не быть. */ export function createChannelTemplate(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template`, { method: 'POST' }) } /** Применяет правила к эфиру: пересобирает будущий хвост. Правка слотов эфир не двигает. */ export function applyChannelTemplate(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/apply`, { method: 'POST', }) } /** Проверки сетки по правилам — считаются по шаблону, без прогона генератора. */ export function getTemplateIssues(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/issues`) } /** Что изменится в эфире, если применить сейчас. Прогон сухой — лента не трогается. */ export function getApplyDiff(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/diff`) } /** Копия сетки на другой канал: слои, слоты, стыки и правила. Группы общие и не копируются. */ export function copyTemplateTo(channelId: string, targetChannelId: string) { return apiRequest( `/admin/channels/${channelId}/template/copy-to/${targetChannelId}`, { method: 'POST' }, ) } /** Цепочка происхождения записи, записанная в момент генерации. */ export function getEntryTrace(entryId: string) { return apiRequest(`/admin/channels/entries/${entryId}/trace`) } /** Сухой прогон по текущим правилам: ничего не пишет и не двигает курсоры слотов. */ export function previewTemplate(channelId: string, days: number) { const query = new URLSearchParams({ days: String(days) }) return apiRequest( `/admin/channels/${channelId}/template/preview?${query.toString()}`, ) } export function updateTemplate( templateId: string, body: { name: string fallbackGroupId: string | null defaultJunctionId: string | null rules: PlanningRules | null }, ) { return apiRequest(`/admin/templates/${templateId}`, { method: 'PUT', body }) } export function createLayer(templateId: string, body: { name: string; priority: number }) { return apiRequest(`/admin/templates/${templateId}/layers`, { method: 'POST', body, }) } export function updateLayer( layerId: string, body: { name: string priority: number applicability: LayerApplicability | null isEnabled: boolean }, ) { return apiRequest(`/admin/layers/${layerId}`, { method: 'PUT', body }) } export function deleteLayer(layerId: string) { return apiRequest(`/admin/layers/${layerId}`, { method: 'DELETE' }) } /** Тело слота: то же для создания и правки (см. SlotInput на сервере). */ export type SlotBody = Omit /** Слот из ответа сервера → тело запроса: отбрасываем то, что сервер проставляет сам. */ export function toSlotBody(slot: SlotDto): SlotBody { const { id: _id, layerId: _layerId, groupName: _groupName, ...body } = slot return body } export function createSlot(layerId: string, body: SlotBody) { return apiRequest(`/admin/layers/${layerId}/slots`, { method: 'POST', body }) } export function updateSlot(slotId: string, body: SlotBody) { return apiRequest(`/admin/slots/${slotId}`, { method: 'PUT', body }) } export function deleteSlot(slotId: string) { return apiRequest(`/admin/slots/${slotId}`, { method: 'DELETE' }) } // ── Стыки канала ────────────────────────────────────────────────────────── export function listJunctions(channelId: string) { return apiRequest(`/admin/channels/${channelId}/junctions`) } export function createJunction(channelId: string, name: string) { return apiRequest(`/admin/channels/${channelId}/junctions`, { method: 'POST', body: { name }, }) } export function renameJunction(junctionId: string, name: string) { return apiRequest(`/admin/junctions/${junctionId}`, { method: 'PUT', body: { name } }) } export function deleteJunction(junctionId: string) { return apiRequest(`/admin/junctions/${junctionId}`, { method: 'DELETE' }) } export function addJunctionElement(junctionId: string, kind: JunctionElementKind) { return apiRequest(`/admin/junctions/${junctionId}/elements`, { method: 'POST', body: { kind }, }) } /** Тело врезки: то же для любого типа — лишние поля сервер обнуляет сам (см. JunctionElement.Update). */ export type JunctionElementBody = { kind: JunctionElementKind groupId: string | null bumperTemplateId: string | null amountMode: JunctionAmountMode amountValue: number isRequired: boolean conditions: JunctionConditions | null } export function updateJunctionElement( junctionId: string, elementId: string, body: JunctionElementBody, ) { return apiRequest(`/admin/junctions/${junctionId}/elements/${elementId}`, { method: 'PUT', body, }) } export function removeJunctionElement(junctionId: string, elementId: string) { return apiRequest(`/admin/junctions/${junctionId}/elements/${elementId}`, { method: 'DELETE', }) } /** Порядок врезок: не упомянутые остаются после перечисленных. */ export function reorderJunction(junctionId: string, elementIdsInOrder: string[]) { return apiRequest(`/admin/junctions/${junctionId}/order`, { method: 'PUT', body: { elementIdsInOrder }, }) } type BumperTemplateStyleBody = { name: string backgroundColor: string backgroundColor2: string accentColor: string textColor: string } export function addBumperTemplate(id: string, name: string) { return apiRequest(`/admin/channels/${id}/bumper/templates`, { method: 'POST', body: { name }, }) } export function updateBumperTemplate(id: string, templateId: string, body: BumperTemplateStyleBody) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}`, { method: 'PUT', body, }) } export function removeBumperTemplate(id: string, templateId: string) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}`, { method: 'DELETE', }) } /** Загрузка сырого файла блока (звук/фон): тело — файл, имя — в query (как в uploadMedia). */ function uploadBumperTemplateFile( id: string, templateId: string, kind: 'audio' | 'background', file: File, ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() const query = new URLSearchParams({ fileName: file.name }) xhr.open( 'PUT', `/api/admin/channels/${id}/bumper/templates/${templateId}/${kind}?${query.toString()}`, ) const token = getAccessToken() if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`) xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve() } else { let detail = `HTTP ${xhr.status}` try { const problem = JSON.parse(xhr.responseText) as { detail?: string; title?: string } detail = problem.detail ?? problem.title ?? detail } catch { /* пусто */ } reject(new HttpError({ detail }, xhr.status)) } } xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0)) xhr.send(file) }) } export function uploadBumperTemplateAudio(id: string, templateId: string, file: File) { return uploadBumperTemplateFile(id, templateId, 'audio', file) } type BumperVariantBody = { name: string kind: BumperTextKind nowLabel: string nextLabel: string line1: string line2: string trigger: BumperTrigger weight: number } export function addBumperVariant(id: string, templateId: string, name: string) { return apiRequest( `/admin/channels/${id}/bumper/templates/${templateId}/variants`, { method: 'POST', body: { name } }, ) } export function updateBumperVariant( id: string, templateId: string, variantId: string, body: BumperVariantBody, ) { return apiRequest( `/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`, { method: 'PUT', body }, ) } export function removeBumperVariant(id: string, templateId: string, variantId: string) { return apiRequest( `/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`, { method: 'DELETE' }, ) } /** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */ export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}/background`, { method: 'PUT', body: { imageId }, }) } export function clearBumperTemplateAudio(id: string, templateId: string) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}/audio`, { method: 'DELETE', }) } export function clearBumperTemplateBackground(id: string, templateId: string) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}/background`, { method: 'DELETE', }) } /** Синхронно рендерит примеры всех подблоков блока (сервер собирает ffmpeg-клипы). */ export function renderBumperPreviews(id: string, templateId: string) { return apiRequest(`/admin/channels/${id}/bumper/templates/${templateId}/preview`, { method: 'POST', }) } export function bumperPreviewPlaylistUrl(id: string, templateId: string, variantId: string) { return `/api/admin/channels/${id}/bumper/templates/${templateId}/preview/${variantId}/index.m3u8` } export function getSchedule(id: string, from: Date, to: Date) { const query = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() }) return apiRequest(`/admin/channels/${id}/schedule?${query.toString()}`) }