Refactor ChannelDetail and MediaImportDialog components for improved functionality and clarity
ci / build-backend (push) Successful in 1m32s
ci / build-frontend (push) Successful in 1m6s
ci / tests (push) Successful in 1m42s
ci / sonar (push) Successful in 4m39s

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.
This commit is contained in:
Leonid Pershin
2026-07-28 10:37:11 +03:00
parent ef5c56f04b
commit 287ed5aa12
7 changed files with 140 additions and 22 deletions
@@ -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' && (
<Card>
<CardContent>
<SchedulePreview entries={schedule ?? []} onShowTrace={setTraceEntryId} />
<AirSchedule
channelId={channelId}
utcOffsetMinutes={channel.utcOffsetMinutes}
dayStartTime={channel.dayStartTime}
onShowTrace={setTraceEntryId}
/>
</CardContent>
</Card>
)}
@@ -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 (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
{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 (
<Button
key={index}
size="sm"
variant={index === day ? 'default' : 'outline'}
onClick={() => {
setDay(index)
setPage(1)
}}
>
{index === 0 ? t('admin.channels.airToday') : label}
</Button>
)
})}
</div>
{isLoading ? (
<p className="text-muted-foreground">{t('common.loading')}</p>
) : (
<>
<SchedulePreview entries={pageItems} onShowTrace={onShowTrace} />
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span>{t('admin.channels.airCount', { count: entries.length })}</span>
<Pager page={page} totalPages={totalPages} onChange={setPage} />
</div>
</>
)}
</div>
)
}
@@ -64,7 +64,7 @@ export function SchedulePreview({
return (
<ul className="flex flex-col divide-y divide-border text-sm">
{entries.slice(0, 40).map((e) => (
{entries.map((e) => (
<li key={e.id} className="group flex items-center gap-3 py-1.5">
<span className="shrink-0 whitespace-nowrap tabular-nums text-muted-foreground">
{formatTime(e.startsAtUtc)}