- {t('admin.channels.group')}
- patch({ groupId: e.target.value || null })}
- >
- {t('admin.channels.pickGroup')}
- {(groups ?? []).map((group) => (
-
- {group.name} · {group.itemCount}
-
- ))}
-
+ {t('admin.channels.slotTitle')}
+ patch({ title: e.target.value })} />
-
- {t('admin.channels.strategy')}
-
- patch({
- strategy: {
- ...(body.strategy ?? {
- restartOnEnd: true,
- cooldownDays: 0,
- fallback: 'OldestFirst',
- type: 'Sequential',
- }),
- type: e.target.value as SlotStrategyType,
- },
- })
- }
- >
- {STRATEGIES.map((value) => (
-
- {t(`admin.channels.strategies.${value}`)}
-
- ))}
-
-
-
- {body.strategy?.type === 'RandomWithCooldown' && (
-
-
{t('admin.channels.cooldownDays')}
-
- patch({
- strategy: { ...body.strategy!, cooldownDays: Number(e.target.value) },
- })
- }
- />
-
{t('admin.channels.cooldownHint')}
-
- )}
-
+
+
+ {t('admin.channels.weekday')}
+
+ patch({ weekday: e.target.value === 'any' ? null : Number(e.target.value) })
+ }
+ >
+ {t('admin.channels.everyDay')}
+ {[1, 2, 3, 4, 5, 6, 0].map((day) => (
+
+ {t(`admin.channels.weekdays.${day}`)}
+
+ ))}
+
+
+
+
+
+ {t('admin.channels.daypart')}
patch({ blockMode: e.target.value as SlotBlockMode })}
+ value={body.daypart}
+ onChange={(e) => patch({ daypart: e.target.value as Daypart })}
>
- {BLOCK_MODES.map((value) => (
+ {DAYPARTS.map((value) => (
- {t(`admin.channels.blockModes.${value}`)}
+ {t(`admin.channels.dayparts.${value}`)}
))}
- {body.blockMode !== 'FillSlot' && (
+
+ {t('admin.channels.slotKind')}
+ {
+ const slotKind = e.target.value as SlotKind
+ patch({
+ slotKind,
+ // Повтору нужен источник, конец вещания не берёт контент вовсе.
+ repeatSource:
+ slotKind === 'Repeat'
+ ? (body.repeatSource ?? {
+ daysAgo: 1,
+ time: '20:00:00',
+ durationMinutes: 90,
+ })
+ : null,
+ })
+ }}
+ >
+ {SLOT_KINDS.map((value) => (
+
+ {t(`admin.channels.slotKinds.${value}`)}
+
+ ))}
+
+
+
+
+ {isContent && (
+ <>
- {t('admin.channels.blockValue')}
+ {t('admin.channels.group')}
+ patch({ groupId: e.target.value || null })}
+ >
+ {t('admin.channels.pickGroup')}
+ {(groups ?? []).map((group) => (
+
+ {group.name} · {group.itemCount}
+
+ ))}
+
+
+
+
+ {t('admin.channels.strategy')}
+
+ patch({
+ strategy: {
+ ...(body.strategy ?? {
+ restartOnEnd: true,
+ cooldownDays: 0,
+ fallback: 'OldestFirst',
+ type: 'Sequential',
+ }),
+ type: e.target.value as SlotStrategyType,
+ },
+ })
+ }
+ >
+ {STRATEGIES.map((value) => (
+
+ {t(`admin.channels.strategies.${value}`)}
+
+ ))}
+
+
+
+ {body.strategy?.type === 'RandomWithCooldown' && (
+
+
{t('admin.channels.cooldownDays')}
+
+ patch({
+ strategy: { ...body.strategy!, cooldownDays: Number(e.target.value) },
+ })
+ }
+ />
+
+ {t('admin.channels.cooldownHint')}
+
+
+ )}
+
+
+
+ {t('admin.channels.blockMode')}
+ patch({ blockMode: e.target.value as SlotBlockMode })}
+ >
+ {BLOCK_MODES.map((value) => (
+
+ {t(`admin.channels.blockModes.${value}`)}
+
+ ))}
+
+
+ {body.blockMode !== 'FillSlot' && (
+
+ {t('admin.channels.blockValue')}
+ patch({ blockValue: Number(e.target.value) })}
+ />
+
+ )}
+
+
+
+
{t('admin.channels.overflow')}
+
patch({ overflowPolicy: e.target.value as OverflowPolicy })}
+ >
+ {OVERFLOW.map((value) => (
+
+ {t(`admin.channels.overflows.${value}`)}
+
+ ))}
+
+
{t('admin.channels.overflowHint')}
+
+ >
+ )}
+
+ {body.slotKind === 'Repeat' && body.repeatSource && (
+
+
+ {t('admin.channels.repeatDaysAgo')}
patch({ blockValue: Number(e.target.value) })}
+ value={body.repeatSource.daysAgo}
+ onChange={(e) =>
+ patch({
+ repeatSource: { ...body.repeatSource!, daysAgo: Number(e.target.value) },
+ })
+ }
/>
- )}
-
+
+ {t('admin.channels.repeatTime')}
+
+ patch({ repeatSource: { ...body.repeatSource!, time: `${e.target.value}:00` } })
+ }
+ />
+
+
+ {t('admin.channels.repeatDuration')}
+
+ patch({
+ repeatSource: {
+ ...body.repeatSource!,
+ durationMinutes: Number(e.target.value),
+ },
+ })
+ }
+ />
+
+
+ )}
-
-
{t('admin.channels.overflow')}
-
patch({ overflowPolicy: e.target.value as OverflowPolicy })}
- >
- {OVERFLOW.map((value) => (
-
- {t(`admin.channels.overflows.${value}`)}
-
- ))}
-
-
{t('admin.channels.overflowHint')}
+
- >
- )}
+
{t('admin.channels.anchorHint')}
- {body.slotKind === 'Repeat' && body.repeatSource && (
-
-
- {t('admin.channels.repeatDaysAgo')}
-
- patch({
- repeatSource: { ...body.repeatSource!, daysAgo: Number(e.target.value) },
- })
- }
- />
-
-
- {t('admin.channels.repeatTime')}
-
- patch({ repeatSource: { ...body.repeatSource!, time: `${e.target.value}:00` } })
- }
- />
-
-
-
{t('admin.channels.repeatDuration')}
-
- patch({
- repeatSource: {
- ...body.repeatSource!,
- durationMinutes: Number(e.target.value),
- },
- })
- }
- />
+ {/* Стыки: внутри слота — между единицами, после слота — на переходе к следующему. */}
+
+
+ {t('admin.channels.junctionBetween')}
+ patch({ junctionBetweenId: e.target.value || null })}
+ >
+ {t('admin.channels.junctionDefault')}
+ {(junctions ?? []).map((junction) => (
+
+ {junction.name}
+
+ ))}
+
+
+
+ {t('admin.channels.junctionAfter')}
+ patch({ junctionAfterId: e.target.value || null })}
+ >
+ {t('admin.channels.junctionDefault')}
+ {(junctions ?? []).map((junction) => (
+
+ {junction.name}
+
+ ))}
+
+
- )}
-
-
- patch({ isAnchor: e.target.checked })}
- />
- {t('admin.channels.anchor')}
-
-
- {t('admin.channels.maxDrift')}
- patch({ maxDriftMinutes: Number(e.target.value) })}
- />
-
-
- {t('admin.channels.snap')}
-
- patch({ snapToMinutes: Number(e.target.value) === 0 ? null : Number(e.target.value) })
- }
- >
- {SNAP_OPTIONS.map((value) => (
-
- {value === 0 ? t('admin.channels.snapOff') : `${value}`}
-
- ))}
-
-
-
-
{t('admin.channels.anchorHint')}
-
- {/* Стыки: внутри слота — между единицами, после слота — на переходе к следующему. */}
-
-
- {t('admin.channels.junctionBetween')}
- patch({ junctionBetweenId: e.target.value || null })}
- >
- {t('admin.channels.junctionDefault')}
- {(junctions ?? []).map((junction) => (
-
- {junction.name}
-
- ))}
-
-
-
- {t('admin.channels.junctionAfter')}
- patch({ junctionAfterId: e.target.value || null })}
- >
- {t('admin.channels.junctionDefault')}
- {(junctions ?? []).map((junction) => (
-
- {junction.name}
-
- ))}
-
-
-
-
-
- {draft.slot ? (
- remove.mutate()}>
-
-
- ) : (
-
- )}
- save.mutate()}>
- {t('common.save')}
-
-
-
+
+ {draft.slot ? (
+ remove.mutate()}>
+
+ {t('common.delete')}
+
+ ) : (
+
+ )}
+
+
+ {t('common.cancel')}
+
+ save.mutate()}>
+ {t('common.save')}
+
+
+
+
+
)
}
diff --git a/frontend/src/features/admin/channels/components/TemplateIssues.tsx b/frontend/src/features/admin/channels/components/TemplateIssues.tsx
index 34eb50f..889bff8 100644
--- a/frontend/src/features/admin/channels/components/TemplateIssues.tsx
+++ b/frontend/src/features/admin/channels/components/TemplateIssues.tsx
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
-import { AlertTriangle, CircleAlert } from 'lucide-react'
+import { AlertTriangle, ChevronDown, ChevronRight, CircleAlert } from 'lucide-react'
+import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { qk } from '@/shared/api/query-keys'
import type { SlotDto, TemplateIssueDto } from '@/shared/api/types'
@@ -9,6 +10,10 @@ import { getTemplateIssues } from '../api'
/**
* Проверки по правилам (см. 5.1). Считаются на сервере по шаблону, без прогона генератора, поэтому
* показываются прямо в редакторе и обновляются вместе с сеткой.
+ *
+ * По умолчанию свёрнуты в одну строку со счётчиками: семь дыр в сетке — обычное состояние
+ * недособранного канала, и занимать ими пол-экрана над самой сеткой значит прятать то, ради чего
+ * экран открывали. Разворачиваются по клику и скроллятся, не растягивая страницу.
*/
export function TemplateIssues({
channelId,
@@ -20,6 +25,7 @@ export function TemplateIssues({
onGoToSlot: (slot: SlotDto) => void
}>) {
const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
const { data: issues } = useQuery({
queryKey: qk.channels.issues(channelId),
queryFn: () => getTemplateIssues(channelId),
@@ -31,20 +37,38 @@ export function TemplateIssues({
const warnings = issues.filter((i) => i.severity === 'Warning')
return (
-
-
+
+
0
+ ? 'border-red-700/50 text-red-500'
+ : 'border-amber-600/50 text-amber-500',
+ )}
+ onClick={() => setOpen((v) => !v)}
+ >
+ {open ? : }
+ {errors.length > 0 ? (
+
+ ) : (
+
+ )}
{t('admin.channels.issues', { errors: errors.length, warnings: warnings.length })}
-
-
- {[...errors, ...warnings].map((issue, index) => (
-
- ))}
-
+
+
+ {open && (
+
+ {[...errors, ...warnings].map((issue, index) => (
+
+ ))}
+
+ )}
)
}