From 287ed5aa12ee746e10e184df1f14c00c284a52c5 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 28 Jul 2026 10:37:11 +0300 Subject: [PATCH] Refactor ChannelDetail and MediaImportDialog components for improved functionality and clarity Consolidated imports in ChannelDetail and replaced the SchedulePreview component with AirSchedule for enhanced scheduling display. Updated the MediaImportDialog to prioritize the manual tab and adjusted localization strings for clarity in both English and Russian. These changes aim to streamline user interactions and improve the overall user experience in managing channels and media imports. --- .../features/admin/channels/ChannelDetail.tsx | 25 ++-- .../admin/channels/components/AirSchedule.tsx | 113 ++++++++++++++++++ .../channels/components/SchedulePreview.tsx | 2 +- .../admin/media/MediaImportDialog.tsx | 13 +- frontend/src/shared/api/query-keys.ts | 1 + frontend/src/shared/lib/locales/en.ts | 4 +- frontend/src/shared/lib/locales/ru.ts | 4 +- 7 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 frontend/src/features/admin/channels/components/AirSchedule.tsx diff --git a/frontend/src/features/admin/channels/ChannelDetail.tsx b/frontend/src/features/admin/channels/ChannelDetail.tsx index 52b0238..1f3ad52 100644 --- a/frontend/src/features/admin/channels/ChannelDetail.tsx +++ b/frontend/src/features/admin/channels/ChannelDetail.tsx @@ -11,19 +11,13 @@ 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, - restoreChannelTemplate, -} from './api' +import { applyChannelTemplate, getChannel, getChannelTemplate, restoreChannelTemplate } from './api' +import { AirSchedule } from './components/AirSchedule' import { ApplyDialog } from './components/ApplyDialog' 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' @@ -50,13 +44,11 @@ export function ChannelDetail({ channelId }: Readonly<{ channelId: string }>) { queryKey: qk.media.ready, queryFn: () => listAllMedia({ statuses: ['Ready'] }), }) - const { data: schedule } = useQuery({ - queryKey: qk.channels.schedule(channelId), - queryFn: () => getSchedule(channelId, new Date(), new Date(Date.now() + 12 * 3_600_000)), - }) - const invalidate = () => { void queryClient.invalidateQueries({ queryKey: qk.channels.detail(channelId) }) + // Применение пересобирает хвост ленты — открытые сутки эфира надо перечитать, иначе на + // вкладке останется то, что уже заменено. + void queryClient.invalidateQueries({ queryKey: qk.channels.schedule(channelId) }) } const onError = useApiError() @@ -190,7 +182,12 @@ export function ChannelDetail({ channelId }: Readonly<{ channelId: string }>) { {tab === 'air' && ( - + )} diff --git a/frontend/src/features/admin/channels/components/AirSchedule.tsx b/frontend/src/features/admin/channels/components/AirSchedule.tsx new file mode 100644 index 0000000..4b0740b --- /dev/null +++ b/frontend/src/features/admin/channels/components/AirSchedule.tsx @@ -0,0 +1,113 @@ +import { useQuery } from '@tanstack/react-query' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { qk } from '@/shared/api/query-keys' +import { Button } from '@/shared/ui/button' +import { Pager } from '@/shared/ui/pager' +import { getSchedule } from '../api' +import { SchedulePreview } from './SchedulePreview' + +/** Сколько вещательных суток показываем: столько же, на сколько по умолчанию строится лента. */ +const DAYS = 7 + +const PAGE_SIZE = 50 + +/** «06:00:00» → минуты от полуночи. */ +function minutesOf(time: string): number { + const [hours, minutes] = time.split(':').map(Number) + return (hours || 0) * 60 + (minutes || 0) +} + +/** + * Начало вещательных суток номер `index`, считая от текущих, в UTC. Сутки канала начинаются + * не в полночь, поэтому «сегодня» до `dayStartTime` — это ещё вчерашний эфир. + */ +function dayStartUtc(index: number, utcOffsetMinutes: number, dayStartTime: string): Date { + const offset = utcOffsetMinutes * 60_000 + const local = new Date(Date.now() + offset) + + const midnight = Date.UTC(local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate()) + const start = midnight + minutesOf(dayStartTime) * 60_000 + const today = local.getTime() >= start ? start : start - 24 * 3_600_000 + + return new Date(today + index * 24 * 3_600_000 - offset) +} + +/** + * Эфир канала: вещательные сутки выбираются кнопками, внутри суток лента листается страницами. + * + * Грузим сутки, а не всю неделю разом: неделя эфира — это тысячи записей вместе с рекламой + * и заставками, а смотрят их всегда по дням, как программу передач. + */ +export function AirSchedule({ + channelId, + utcOffsetMinutes, + dayStartTime, + onShowTrace, +}: Readonly<{ + channelId: string + utcOffsetMinutes: number + /** Начало вещательных суток во времени канала («06:00:00»). */ + dayStartTime: string + onShowTrace: (entryId: string) => void +}>) { + const { t } = useTranslation() + const [day, setDay] = useState(0) + const [page, setPage] = useState(1) + + const range = useMemo(() => { + const from = dayStartUtc(day, utcOffsetMinutes, dayStartTime) + return { from, to: new Date(from.getTime() + 24 * 3_600_000) } + }, [day, utcOffsetMinutes, dayStartTime]) + + const { data, isLoading } = useQuery({ + queryKey: qk.channels.scheduleDay(channelId, range.from.toISOString()), + queryFn: () => getSchedule(channelId, range.from, range.to), + }) + + const entries = data ?? [] + const totalPages = Math.max(1, Math.ceil(entries.length / PAGE_SIZE)) + const pageItems = entries.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE) + + return ( +
+
+ {Array.from({ length: DAYS }, (_, index) => { + const start = dayStartUtc(index, utcOffsetMinutes, dayStartTime) + // Подпись — дата во времени канала: сетка задаётся в нём, и часовой пояс админа тут + // только сбивал бы. + const label = new Date(start.getTime() + utcOffsetMinutes * 60_000).toLocaleDateString( + [], + { weekday: 'short', day: '2-digit', month: '2-digit', timeZone: 'UTC' }, + ) + + return ( + + ) + })} +
+ + {isLoading ? ( +

{t('common.loading')}

+ ) : ( + <> + +
+ {t('admin.channels.airCount', { count: entries.length })} + +
+ + )} +
+ ) +} diff --git a/frontend/src/features/admin/channels/components/SchedulePreview.tsx b/frontend/src/features/admin/channels/components/SchedulePreview.tsx index 4eb0dc9..70b324c 100644 --- a/frontend/src/features/admin/channels/components/SchedulePreview.tsx +++ b/frontend/src/features/admin/channels/components/SchedulePreview.tsx @@ -64,7 +64,7 @@ export function SchedulePreview({ return (