Enhance channel and settings functionalities: introduce viewer settings in ChannelEndpoints, update SiteSettings to include channel number toggling, and refactor related data structures. Implement new endpoints for validating templates and diffing scheduling changes, improving overall user experience and configuration management.
This commit is contained in:
@@ -12,25 +12,31 @@ import { Input } from '@/shared/ui/input'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import {
|
||||
applyChannelTemplate,
|
||||
copyTemplateTo,
|
||||
createLayer,
|
||||
createSlot,
|
||||
deleteLayer,
|
||||
getChannel,
|
||||
getChannelTemplate,
|
||||
getSchedule,
|
||||
listChannels,
|
||||
toSlotBody,
|
||||
updateLayer,
|
||||
updateSlot,
|
||||
} from './api'
|
||||
import { ApplyDialog } from './components/ApplyDialog'
|
||||
import { BumperCard } from './components/BumperCard'
|
||||
import { CollapsibleCard } from './components/CollapsibleCard'
|
||||
import { EntryTraceDialog } from './components/EntryTraceDialog'
|
||||
import { JunctionsCard } from './components/JunctionsCard'
|
||||
import { LayerApplicabilityDialog } from './components/LayerApplicabilityDialog'
|
||||
import { LayerList, ScheduleGrid } from './components/ScheduleGrid'
|
||||
import { SchedulePreview } from './components/SchedulePreview'
|
||||
import { RulesCard } from './components/RulesCard'
|
||||
import { SettingsCard } from './components/SettingsCard'
|
||||
import { TemplateIssues } from './components/TemplateIssues'
|
||||
import { TemplatePreview } from './components/TemplatePreview'
|
||||
import { ViewerCard } from './components/ViewerCard'
|
||||
import { SlotInspector, type SlotDraft } from './components/SlotInspector'
|
||||
import { toTime } from './lib/format'
|
||||
|
||||
@@ -44,6 +50,9 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
// День, который копируем, и отмеченные дни-приёмники.
|
||||
const [copySource, setCopySource] = useState<number | null>(null)
|
||||
const [copyTargets, setCopyTargets] = useState<number[]>([])
|
||||
const [applyOpen, setApplyOpen] = useState(false)
|
||||
const [traceEntryId, setTraceEntryId] = useState<string | null>(null)
|
||||
const [copyToChannel, setCopyToChannel] = useState('')
|
||||
|
||||
const { data: channel, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'channels', channelId],
|
||||
@@ -68,9 +77,12 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
const onError = (error: unknown) =>
|
||||
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
|
||||
|
||||
const { data: channels } = useQuery({ queryKey: ['admin', 'channels'], queryFn: listChannels })
|
||||
|
||||
const applyMutation = useMutation({
|
||||
mutationFn: () => applyChannelTemplate(channelId),
|
||||
onSuccess: (result) => {
|
||||
setApplyOpen(false)
|
||||
toast.success(t('admin.channels.applied', { count: result.added }))
|
||||
// Предупреждения показываем по одному: каждое указывает на конкретный слот.
|
||||
for (const warning of result.warnings)
|
||||
@@ -136,6 +148,21 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
onError,
|
||||
})
|
||||
|
||||
const copyTemplateMutation = useMutation({
|
||||
mutationFn: (targetChannelId: string) => copyTemplateTo(channelId, targetChannelId),
|
||||
onSuccess: (result) => {
|
||||
setCopyToChannel('')
|
||||
toast.success(
|
||||
t('admin.channels.templateCopied', { layers: result.layers, slots: result.slots }),
|
||||
)
|
||||
if (result.droppedBumperRefs > 0)
|
||||
toast.error(
|
||||
t('admin.channels.copyDroppedBumpers', { count: result.droppedBumperRefs }),
|
||||
)
|
||||
},
|
||||
onError,
|
||||
})
|
||||
|
||||
const moveSlotMutation = useMutation({
|
||||
mutationFn: ({
|
||||
slot,
|
||||
@@ -215,7 +242,7 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
{template?.hasPendingChanges && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-500/50 bg-amber-500/10 px-4 py-2 text-sm">
|
||||
<span>{t('admin.channels.pendingChanges')}</span>
|
||||
<Button size="sm" disabled={applyMutation.isPending} onClick={() => applyMutation.mutate()}>
|
||||
<Button size="sm" disabled={applyMutation.isPending} onClick={() => setApplyOpen(true)}>
|
||||
<Send className="h-4 w-4" /> {t('admin.channels.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -256,9 +283,48 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
onEditApplicability={setApplicabilityLayer}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.channels.layersHint')}</p>
|
||||
|
||||
{/* Копия сетки на другой канал: группы общие, поэтому переносятся только правила. */}
|
||||
<div className="flex flex-col gap-1.5 border-t border-border pt-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{t('admin.channels.copyTemplate')}
|
||||
</span>
|
||||
<select
|
||||
className="h-8 rounded-md border border-border bg-transparent px-2 text-sm"
|
||||
value={copyToChannel}
|
||||
onChange={(e) => setCopyToChannel(e.target.value)}
|
||||
>
|
||||
<option value="">{t('admin.channels.pickTargetChannel')}</option>
|
||||
{(channels ?? [])
|
||||
.filter((c) => c.id !== channelId)
|
||||
.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!copyToChannel || copyTemplateMutation.isPending}
|
||||
onClick={() => copyTemplateMutation.mutate(copyToChannel)}
|
||||
>
|
||||
{t('admin.channels.copy')}
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('admin.channels.copyTemplateHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<TemplateIssues
|
||||
channelId={channelId}
|
||||
slotsById={
|
||||
new Map(template.layers.flatMap((l) => l.slots).map((slot) => [slot.id, slot]))
|
||||
}
|
||||
onGoToSlot={openSlot}
|
||||
/>
|
||||
<TemplatePreview channelId={channelId} />
|
||||
|
||||
{/* Сетка на конкретную дату: видно, какие слои в этот день действительно действуют. */}
|
||||
@@ -359,6 +425,8 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
|
||||
<BumperCard channel={channel} onSaved={invalidate} onError={onError} />
|
||||
|
||||
<ViewerCard channel={channel} onSaved={invalidate} onError={onError} />
|
||||
|
||||
{applicabilityLayer && (
|
||||
<LayerApplicabilityDialog
|
||||
layer={applicabilityLayer}
|
||||
@@ -368,7 +436,25 @@ export function ChannelDetail({ channelId }: { channelId: string }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SchedulePreview entries={schedule ?? []} />
|
||||
<SchedulePreview entries={schedule ?? []} onShowTrace={setTraceEntryId} />
|
||||
|
||||
{applyOpen && (
|
||||
<ApplyDialog
|
||||
channelId={channelId}
|
||||
utcOffsetMinutes={channel.utcOffsetMinutes}
|
||||
pending={applyMutation.isPending}
|
||||
onApply={() => applyMutation.mutate()}
|
||||
onClose={() => setApplyOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{traceEntryId && (
|
||||
<EntryTraceDialog
|
||||
entryId={traceEntryId}
|
||||
utcOffsetMinutes={channel.utcOffsetMinutes}
|
||||
onClose={() => setTraceEntryId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user