import { apiDownload, apiRequest } from '@/shared/api/client' import type { ApplyResultDto, ChannelDto, ChannelSummaryDto, CopyTemplateResultDto, CreatedIdResponse, EntryTraceDto, GenerateGridResultDto, GridConfig, GridGenerationMode, GridImportResultDto, GridPromptDto, GridPlanDto, GridProfileDto, GridProfileKind, LayerApplicability, PlanningRules, ResetTemplateResultDto, RestoreTemplateResultDto, 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 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 restoreChannelTemplate(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/restore`, { method: 'POST', }) } /** * Полный сброс: снимает сетку и удаляет будущий эфир, собранный по ней. Прошлое и идущая сейчас * запись остаются — их нельзя вырезать из-под зрителя. */ export function resetChannelTemplate(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/reset`, { 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 listGridProfiles() { return apiRequest('/admin/grid-profiles') } /** План автосборки: что будет создано и что снесено. Считается тем же кодом, что и сама сборка. */ export function previewGrid(channelId: string, profile: GridProfileKind, mode: GridGenerationMode) { const query = new URLSearchParams({ profile, mode }) return apiRequest( `/admin/channels/${channelId}/template/grid-plan?${query.toString()}`, ) } /** Собирает сетку по профилю. План пересчитывается на сервере — с клиента едут только опции. */ export function generateGrid( channelId: string, profile: GridProfileKind, mode: GridGenerationMode, ) { return apiRequest(`/admin/channels/${channelId}/template/generate`, { method: 'POST', body: { profile, mode }, }) } /** Выгрузка сетки одним файлом: перенос, бэкап перед экспериментом и образец для ИИ. */ export function exportGrid(channelId: string) { return apiRequest(`/admin/channels/${channelId}/template/export`) } /** * Загрузка сетки из файла. `replace` — снести существующие слоты и построить заново; иначе слои * и слоты добавляются к тому, что уже есть. */ export function importGrid(channelId: string, config: GridConfig, replace: boolean) { return apiRequest(`/admin/channels/${channelId}/template/import`, { method: 'POST', body: { config, replace }, }) } /** * Собирает текст запроса к ИИ по референс-каналам и пожеланиям. Никуда не отправляется — админ * копирует его в свою модель и приносит ответ назад кнопкой импорта. */ export function buildGridPrompt(channelId: string, references: string[], notes: string) { return apiRequest(`/admin/channels/${channelId}/template/ai-prompt`, { method: 'POST', body: { references, notes: notes || null }, }) } /** Копия сетки на другой канал: слои, слоты, стыки и правила. Группы общие и не копируются. */ 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 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()}`) } /** * Отладочный дамп канала: вход планировщика, состояние слотов, лента и сухой прогон одним архивом. * Тянем через fetch, а не ссылкой: эндпоинт закрыт Bearer'ом, и `` заголовок не отправит. */ export async function exportChannelDebug(channelId: string) { const { blob, fileName } = await apiDownload( `/admin/channels/${channelId}/debug-export`, 'telewave-debug.zip', { method: 'POST' }, ) const url = URL.createObjectURL(blob) try { const link = document.createElement('a') link.href = url link.download = fileName link.click() } finally { URL.revokeObjectURL(url) } return fileName }