-
-
-
-
- {t('admin.channels.title')}
-
-
-
-
-
{channel.name}
- {channel.number !== null && № {channel.number} }
- {!channel.isEnabled && {t('admin.channels.disabled')} }
-
-
-
- {/* Правка правил эфира не двигает — применение отдельной кнопкой. */}
- {template?.hasPendingChanges && (
-
- {t('admin.channels.pendingChanges')}
- setApplyOpen(true)}>
- {t('admin.channels.apply')}
-
-
- )}
-
- {/* Вкладки вместо колонки карточек: экран канала перестал помещаться в один свиток. */}
-
- {TABS.map((value) => (
- setTab(value)}
- className={cn(
- 'pb-2 text-muted-foreground hover:text-foreground',
- tab === value && 'border-b-2 border-primary text-primary',
- )}
- >
- {t(`admin.channels.tabs.${value}`)}
-
- ))}
-
-
- {tab === 'settings' && (
-
- )}
-
- {/* Шаблон мог не загрузиться — раньше вкладка сетки просто оказывалась пустой. */}
- {tab === 'grid' && !template && (
-
-
- {templateError instanceof HttpError
- ? templateError.detail
- : t('admin.channels.noTemplate')}
-
-
createTemplateMutation.mutate()}
- >
- {t('admin.channels.createTemplate')}
-
-
{t('admin.channels.createTemplateHint')}
-
- )}
-
- {tab === 'grid' && template && (
-
-
-
-
-
-
- {t('admin.channels.layers')}
-
-
addLayerMutation.mutate()}
- disabled={addLayerMutation.isPending}
- >
-
-
-
-
setActiveLayerId(layer.id)}
- onDelete={(layer) => deleteLayerMutation.mutate(layer)}
- onToggle={(layer) => toggleLayerMutation.mutate(layer)}
- onReorder={(order) => reorderLayersMutation.mutate(order)}
- onEditApplicability={setApplicabilityLayer}
- />
- {t('admin.channels.layersHint')}
-
- {/* Копия сетки на другой канал: группы общие, поэтому переносятся только правила. */}
-
-
- {t('admin.channels.copyTemplate')}
-
-
setCopyFromChannel(e.target.value)}
- >
- {t('admin.channels.pickSourceChannel')}
- {(channels ?? [])
- .filter((c) => c.id !== channelId)
- .map((c) => (
-
- {c.name}
-
- ))}
-
-
{
- // Замена своей сетки — необратимая правка, поэтому спрашиваем перед ней.
- if (!window.confirm(t('admin.channels.copyTemplateConfirm'))) return
- copyTemplateMutation.mutate(copyFromChannel)
- }}
- >
- {t('admin.channels.copyHere')}
-
-
- {t('admin.channels.copyTemplateHint')}
-
-
-
-
-
-
l.slots).map((slot) => [slot.id, slot]))
- }
- onGoToSlot={openSlot}
- />
-
-
- {/* Сетка на конкретную дату: видно, какие слои в этот день действительно действуют. */}
-
- {t('admin.channels.showForDate')}
- setViewDate(e.target.value)}
- />
- {viewDate && (
- setViewDate('')}>
- {t('admin.channels.allDates')}
-
- )}
-
-
- {/* Копирование дня: сначала выбирается источник, потом дни-приёмники. */}
- {copySource !== null && (
-
-
- {t('admin.channels.copyDayFrom', {
- day: t(`admin.channels.weekdays.${copySource}`),
- })}
-
- {[1, 2, 3, 4, 5, 6, 0]
- .filter((day) => day !== copySource)
- .map((day) => (
-
-
- setCopyTargets((current) =>
- e.target.checked
- ? [...current, day]
- : current.filter((d) => d !== day),
- )
- }
- />
- {t(`admin.channels.weekdays.${day}`)}
-
- ))}
-
- copyDayMutation.mutate({ from: copySource, to: copyTargets })
- }
- >
- {t('admin.channels.copy')}
-
- setCopySource(null)}>
- {t('common.cancel')}
-
-
- )}
-
-
- moveSlotMutation.mutate({ slot, weekday, startMinutes })
- }
- onResizeSlot={(slot, minutes) => resizeSlotMutation.mutate({ slot, minutes })}
- onCopyDay={(weekday) => {
- setCopySource(weekday)
- setCopyTargets([])
- }}
- />
- {draft && (
- setDraft(null)}
- onChanged={invalidate}
- />
- )}
-
-
-
-
- )}
-
- {tab === 'rules' &&
- (template ? (
-
- ) : (
-
{t('admin.channels.noTemplate')}
- ))}
-
- {tab === 'junctions' && (
-
- )}
-
- {tab === 'bumpers' && (
-
- )}
-
- {tab === 'viewer' && (
-
- )}
-
- {tab === 'air' && (
-
-
-
-
-
- )}
-
- {applicabilityLayer && (
-
setApplicabilityLayer(null)}
- onChanged={invalidate}
- onError={onError}
- />
- )}
-
- {applyOpen && (
- applyMutation.mutate()}
- onClose={() => setApplyOpen(false)}
- />
- )}
-
- {traceEntryId && (
- setTraceEntryId(null)}
- />
- )}
-
- )
-}
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { Link } from '@tanstack/react-router'
+import { ChevronLeft, Send } from 'lucide-react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { listAllMedia } from '@/features/admin/media/api'
+import { qk } from '@/shared/api/query-keys'
+import { cn } from '@/shared/lib/cn'
+import { useApiError } from '@/shared/lib/use-api-error'
+import { Badge } from '@/shared/ui/badge'
+import { Button } from '@/shared/ui/button'
+import { Card, CardContent } from '@/shared/ui/card'
+import { toast } from '@/shared/ui/toast-store'
+import {
+ applyChannelTemplate,
+ getChannel,
+ getChannelTemplate,
+ getSchedule,
+} from './api'
+import { ApplyDialog } from './components/ApplyDialog'
+import { BumperCard } from './components/BumperCard'
+import { EntryTraceDialog } from './components/EntryTraceDialog'
+import { GridTab } from './components/GridTab'
+import { JunctionsCard } from './components/JunctionsCard'
+import { RulesCard } from './components/RulesCard'
+import { SchedulePreview } from './components/SchedulePreview'
+import { SettingsCard } from './components/SettingsCard'
+import { ViewerCard } from './components/ViewerCard'
+
+/** Вкладки экрана канала: настройки первыми — с них канал и начинается. */
+const TABS = ['settings', 'grid', 'rules', 'junctions', 'bumpers', 'viewer', 'air'] as const
+type ChannelTab = (typeof TABS)[number]
+
+export function ChannelDetail({ channelId }: { channelId: string }) {
+ const { t } = useTranslation()
+ const queryClient = useQueryClient()
+ const [applyOpen, setApplyOpen] = useState(false)
+ const [traceEntryId, setTraceEntryId] = useState