diff --git a/backend/src/TeleWave.Application/Common/Models/Result.cs b/backend/src/TeleWave.Application/Common/Models/Result.cs index 17a96dc..d6c6a13 100644 --- a/backend/src/TeleWave.Application/Common/Models/Result.cs +++ b/backend/src/TeleWave.Application/Common/Models/Result.cs @@ -18,10 +18,10 @@ public class Result public static Result Success() => new(true, Error.None); - public static Result Failure(Error error) => new(false, error); - public static Result Success(T value) => new(value, true, Error.None); + public static Result Failure(Error error) => new(false, error); + public static Result Failure(Error error) => new(default, false, error); } diff --git a/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs b/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs index ff3d8da..da34fbc 100644 --- a/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs +++ b/backend/src/TeleWave.Application/Programming/Planning/GroupExpander.cs @@ -195,7 +195,7 @@ public sealed class GroupExpander(IAppDbContext dbContext) e.ChannelId == channelId && e.Kind == ScheduleEntryKind.Program && e.ShowId != null - && showIds.Contains(e.ShowId!.Value) + && showIds.Contains(e.ShowId.Value) ) .GroupBy(e => e.ShowId!.Value) .Select(g => new { ShowId = g.Key, LastPlayed = g.Max(e => e.StartsAtUtc) }) @@ -221,7 +221,7 @@ public sealed class GroupExpander(IAppDbContext dbContext) e.ChannelId == channelId && e.Kind == ScheduleEntryKind.Program && e.ShowId != null - && showIds.Contains(e.ShowId!.Value) + && showIds.Contains(e.ShowId.Value) && e.StartsAtUtc >= since ) .Select(e => new { ShowId = e.ShowId!.Value, e.StartsAtUtc }) diff --git a/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs b/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs index e01f104..75aec3b 100644 --- a/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs +++ b/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs @@ -50,12 +50,8 @@ public sealed record PlanningRules( // Пересекающиеся окна разрешаются в пользу строгого: детское время не должно // отменяться более широким окном, случайно наложенным сверху. - ShowAudience? strictest = null; - foreach (var window in windows.Where(w => w.Contains(moment))) - strictest = strictest is { } current && current <= window.MaxAudience - ? current - : window.MaxAudience; - return strictest; + var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList(); + return applicable.Count > 0 ? applicable.Min() : null; } public string ToJson() => JsonSerializer.Serialize(this, Options); diff --git a/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs b/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs index 3e3ff01..85f3ee4 100644 --- a/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs +++ b/backend/src/TeleWave.Infrastructure/Identity/RoleService.cs @@ -110,7 +110,7 @@ internal sealed class RoleService( if (currentRoles.Count > 0) await userManager.RemoveFromRolesAsync(user, currentRoles); - await userManager.AddToRoleAsync(user, role.Name!); + await userManager.AddToRoleAsync(user, role.Name); return Result.Success(); } diff --git a/frontend/src/features/admin/channels/ChannelDetail.tsx b/frontend/src/features/admin/channels/ChannelDetail.tsx index f894fca..39789d9 100644 --- a/frontend/src/features/admin/channels/ChannelDetail.tsx +++ b/frontend/src/features/admin/channels/ChannelDetail.tsx @@ -31,7 +31,7 @@ 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 }) { +export function ChannelDetail({ channelId }: Readonly<{ channelId: string }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [applyOpen, setApplyOpen] = useState(false) @@ -52,7 +52,7 @@ export function ChannelDetail({ channelId }: { channelId: string }) { }) const { data: schedule } = useQuery({ queryKey: qk.channels.schedule(channelId), - queryFn: () => getSchedule(channelId, new Date(), new Date(Date.now() + 12 * 3600_000)), + queryFn: () => getSchedule(channelId, new Date(), new Date(Date.now() + 12 * 3_600_000)), }) const invalidate = () => { diff --git a/frontend/src/features/admin/channels/components/ApplyDialog.tsx b/frontend/src/features/admin/channels/components/ApplyDialog.tsx index 576bf1e..5851f98 100644 --- a/frontend/src/features/admin/channels/components/ApplyDialog.tsx +++ b/frontend/src/features/admin/channels/components/ApplyDialog.tsx @@ -25,13 +25,13 @@ export function ApplyDialog({ pending, onApply, onClose, -}: { +}: Readonly<{ channelId: string utcOffsetMinutes: number pending: boolean onApply: () => void onClose: () => void -}) { +}>) { const { t } = useTranslation() const { data, isFetching } = useQuery({ queryKey: qk.channels.diff(channelId), diff --git a/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx b/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx index 1381db7..a343d77 100644 --- a/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx +++ b/frontend/src/features/admin/channels/components/BumperBackgroundField.tsx @@ -13,13 +13,13 @@ export function BumperBackgroundField({ backgroundImageId, onChanged, onError, -}: { +}: Readonly<{ channelId: string templateId: string backgroundImageId: string | null onChanged: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [galleryOpen, setGalleryOpen] = useState(false) diff --git a/frontend/src/features/admin/channels/components/BumperCard.tsx b/frontend/src/features/admin/channels/components/BumperCard.tsx index d8aacc1..4b1066a 100644 --- a/frontend/src/features/admin/channels/components/BumperCard.tsx +++ b/frontend/src/features/admin/channels/components/BumperCard.tsx @@ -15,12 +15,12 @@ export function BumperCard({ bare, onSaved, onError, -}: { +}: Readonly<{ channel: ChannelDto bare?: boolean onSaved: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [bumpersEnabled, setBumpersEnabled] = useState(channel.bumpersEnabled) const [bumper, setBumper] = useState(channel.bumper) diff --git a/frontend/src/features/admin/channels/components/BumperFileUpload.tsx b/frontend/src/features/admin/channels/components/BumperFileUpload.tsx index 0485987..c1e833c 100644 --- a/frontend/src/features/admin/channels/components/BumperFileUpload.tsx +++ b/frontend/src/features/admin/channels/components/BumperFileUpload.tsx @@ -15,7 +15,7 @@ export function BumperFileUpload({ clear, onSaved, onError, -}: { +}: Readonly<{ channelId: string templateId: string kind: string @@ -27,7 +27,7 @@ export function BumperFileUpload({ clear: (id: string, templateId: string) => Promise onSaved: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const inputId = `bumper-${kind}-${templateId}` diff --git a/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx b/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx index ce18032..a78e72a 100644 --- a/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx +++ b/frontend/src/features/admin/channels/components/BumperPreviewPlayer.tsx @@ -11,12 +11,12 @@ export function BumperPreviewPlayer({ templateId, variants, onError, -}: { +}: Readonly<{ channelId: string templateId: string variants: BumperTextVariantDto[] onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [ready, setReady] = useState(false) const [bust, setBust] = useState(0) diff --git a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx index cbe1d96..ab08b52 100644 --- a/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx +++ b/frontend/src/features/admin/channels/components/BumperTemplateEditor.tsx @@ -26,12 +26,12 @@ export function BumperTemplateEditor({ template, onChanged, onError, -}: { +}: Readonly<{ channelId: string template: BumperTemplateDto onChanged: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [open, setOpen] = useState(false) const [name, setName] = useState(template.name) diff --git a/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx b/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx index 9fca9ba..322da6d 100644 --- a/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx +++ b/frontend/src/features/admin/channels/components/BumperVariantEditor.tsx @@ -16,14 +16,14 @@ export function BumperVariantEditor({ canRemove, onChanged, onError, -}: { +}: Readonly<{ channelId: string templateId: string variant: BumperTextVariantDto canRemove: boolean onChanged: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [form, setForm] = useState({ name: variant.name, diff --git a/frontend/src/features/admin/channels/components/CollapsibleCard.tsx b/frontend/src/features/admin/channels/components/CollapsibleCard.tsx index 62971c4..3d83e98 100644 --- a/frontend/src/features/admin/channels/components/CollapsibleCard.tsx +++ b/frontend/src/features/admin/channels/components/CollapsibleCard.tsx @@ -9,14 +9,14 @@ export function CollapsibleCard({ bare = false, contentClassName, children, -}: { +}: Readonly<{ title: string defaultOpen?: boolean /** Без своего заголовка и сворачивания — когда карточка и так лежит во вкладке с этим названием. */ bare?: boolean contentClassName?: string children: ReactNode -}) { +}>) { const [open, setOpen] = useState(defaultOpen) if (bare) diff --git a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx index 6d9b3e2..831e415 100644 --- a/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx +++ b/frontend/src/features/admin/channels/components/EntryTraceDialog.tsx @@ -19,11 +19,11 @@ export function EntryTraceDialog({ entryId, utcOffsetMinutes, onClose, -}: { +}: Readonly<{ entryId: string utcOffsetMinutes: number onClose: () => void -}) { +}>) { const { t } = useTranslation() const { data } = useQuery({ queryKey: qk.entries.trace(entryId), @@ -99,7 +99,7 @@ function strategySummary(data: EntryTraceDto, t: Translate) { ]) } -function Row({ label, children }: { label: string; children: React.ReactNode }) { +function Row({ label, children }: Readonly<{ label: string; children: React.ReactNode }>) { return ( <>
{label}
diff --git a/frontend/src/features/admin/channels/components/GridTab.tsx b/frontend/src/features/admin/channels/components/GridTab.tsx index 0d9b378..78af3a7 100644 --- a/frontend/src/features/admin/channels/components/GridTab.tsx +++ b/frontend/src/features/admin/channels/components/GridTab.tsx @@ -38,13 +38,13 @@ export function GridTab({ templateError, onChanged, onError, -}: { +}: Readonly<{ channelId: string template: ScheduleTemplateDto | undefined templateError: unknown onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [draft, setDraft] = useState(null) const [activeLayerId, setActiveLayerId] = useState(null) @@ -141,17 +141,17 @@ export function GridTab({ slot, weekday, startMinutes, - }: { + }: Readonly<{ slot: SlotDto weekday: number startMinutes: number - }) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }), + }>) => updateSlot(slot.id, { ...toSlotBody(slot), weekday, targetStart: toTime(startMinutes) }), onSuccess: onChanged, onError, }) const resizeSlotMutation = useMutation({ - mutationFn: ({ slot, minutes }: { slot: SlotDto; minutes: number }) => + mutationFn: ({ slot, minutes }: Readonly<{ slot: SlotDto; minutes: number }>) => updateSlot(slot.id, { ...toSlotBody(slot), targetDurationMinutes: minutes }), onSuccess: onChanged, onError, @@ -159,7 +159,7 @@ export function GridTab({ /** Копирование дня: слоты «каждый день» не копируются — они и так есть во всех колонках. */ const copyDayMutation = useMutation({ - mutationFn: async ({ from, to }: { from: number; to: number[] }) => { + mutationFn: async ({ from, to }: Readonly<{ from: number; to: number[] }>) => { const sources = (template?.layers ?? []).flatMap((layer) => layer.slots.filter((slot) => slot.weekday === from).map((slot) => ({ layer, slot })), ) diff --git a/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx b/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx index 9b02097..816de1e 100644 --- a/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx +++ b/frontend/src/features/admin/channels/components/JunctionElementDialog.tsx @@ -44,14 +44,14 @@ export function JunctionElementDialog({ onClose, onChanged, onError, -}: { +}: Readonly<{ junctionId: string element: JunctionElementDto bumperTemplates: BumperTemplateDto[] onClose: () => void onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [body, setBody] = useState(() => toBody(element)) const { data: groups } = useQuery({ queryKey: qk.groups.all, queryFn: listGroups }) diff --git a/frontend/src/features/admin/channels/components/JunctionsCard.tsx b/frontend/src/features/admin/channels/components/JunctionsCard.tsx index 5a7c11b..9765832 100644 --- a/frontend/src/features/admin/channels/components/JunctionsCard.tsx +++ b/frontend/src/features/admin/channels/components/JunctionsCard.tsx @@ -86,13 +86,13 @@ export function JunctionsCard({ bare, onChanged, onError, -}: { +}: Readonly<{ channel: ChannelDto template: ScheduleTemplateDto | undefined bare?: boolean onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [newName, setNewName] = useState('') @@ -186,13 +186,13 @@ function JunctionChain({ groups, onChanged, onError, -}: { +}: Readonly<{ junction: JunctionTemplateDto channel: ChannelDto groups: GroupSummaryDto[] | undefined onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [name, setName] = useState(null) const [dragged, setDragged] = useState(null) diff --git a/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx b/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx index c2df1a9..e2a73b2 100644 --- a/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx +++ b/frontend/src/features/admin/channels/components/LayerApplicabilityDialog.tsx @@ -29,12 +29,12 @@ export function LayerApplicabilityDialog({ onClose, onChanged, onError, -}: { +}: Readonly<{ layer: GridLayerDto onClose: () => void onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [name, setName] = useState(layer.name) const [weekdays, setWeekdays] = useState(layer.applicability?.weekdays ?? []) @@ -199,12 +199,12 @@ function Section({ onAdd, empty, children, -}: { +}: Readonly<{ title: string onAdd: () => void empty: boolean children: React.ReactNode -}) { +}>) { const { t } = useTranslation() return (
@@ -228,11 +228,11 @@ function MonthDay({ value, prefix, onChange, -}: { +}: Readonly<{ value: AnnualRange prefix: 'from' | 'to' onChange: (part: Partial) => void -}) { +}>) { const month = prefix === 'from' ? value.fromMonth : value.toMonth const day = prefix === 'from' ? value.fromDay : value.toDay diff --git a/frontend/src/features/admin/channels/components/RulesCard.tsx b/frontend/src/features/admin/channels/components/RulesCard.tsx index b029781..3d872d1 100644 --- a/frontend/src/features/admin/channels/components/RulesCard.tsx +++ b/frontend/src/features/admin/channels/components/RulesCard.tsx @@ -37,12 +37,12 @@ export function RulesCard({ bare, onChanged, onError, -}: { +}: Readonly<{ template: ScheduleTemplateDto bare?: boolean onChanged: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [windows, setWindows] = useState(() => toRows(template.rules?.maxAudienceByTime ?? []), diff --git a/frontend/src/features/admin/channels/components/ScheduleGrid.tsx b/frontend/src/features/admin/channels/components/ScheduleGrid.tsx index eaa81c7..c0e9288 100644 --- a/frontend/src/features/admin/channels/components/ScheduleGrid.tsx +++ b/frontend/src/features/admin/channels/components/ScheduleGrid.tsx @@ -56,7 +56,7 @@ export function ScheduleGrid({ onMoveSlot, onResizeSlot, onCopyDay, -}: { +}: Readonly<{ template: ScheduleTemplateDto selectedSlotId: string | null /** Дата, на которую смотрим сетку («показать 25 декабря»); null — все слои разом. */ @@ -67,7 +67,7 @@ export function ScheduleGrid({ onMoveSlot: (slot: SlotDto, weekday: number, startMinutes: number) => void onResizeSlot: (slot: SlotDto, durationMinutes: number) => void onCopyDay: (fromWeekday: number) => void -}) { +}>) { const { t } = useTranslation() const dayStart = template.dayStartTime.slice(0, 5) const dayStartMinutes = minutesOf(dayStart) @@ -280,7 +280,7 @@ export function LayerList({ onToggle, onReorder, onEditApplicability, -}: { +}: Readonly<{ template: ScheduleTemplateDto activeLayerId: string | null viewDate: string | null @@ -289,7 +289,7 @@ export function LayerList({ onToggle: (layer: GridLayerDto) => void onReorder: (layerIdsTopFirst: string[]) => void onEditApplicability: (layer: GridLayerDto) => void -}) { +}>) { const { t } = useTranslation() const [dragged, setDragged] = useState(null) diff --git a/frontend/src/features/admin/channels/components/SchedulePreview.tsx b/frontend/src/features/admin/channels/components/SchedulePreview.tsx index df162a1..cd4f496 100644 --- a/frontend/src/features/admin/channels/components/SchedulePreview.tsx +++ b/frontend/src/features/admin/channels/components/SchedulePreview.tsx @@ -5,7 +5,7 @@ import { Badge } from '@/shared/ui/badge' import { formatTime } from '../lib/format' /** Что стоит в строке расписания: реклама, заставка-переход или программа с номером серии. */ -function EntryLabel({ entry }: { entry: ScheduleEntryDto }) { +function EntryLabel({ entry }: Readonly<{ entry: ScheduleEntryDto }>) { const { t } = useTranslation() if (entry.kind === 'Ad') return {t('air.ad')} @@ -35,7 +35,7 @@ function EntryLabel({ entry }: { entry: ScheduleEntryDto }) { } /** «· S02E05» либо «· серия N» — что удалось распознать; ничего, если ни того ни другого нет. */ -function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) { +function EpisodeSuffix({ entry }: Readonly<{ entry: ScheduleEntryDto }>) { const { t } = useTranslation() if (entry.seasonEpisode) @@ -54,10 +54,10 @@ function EpisodeSuffix({ entry }: { entry: ScheduleEntryDto }) { export function SchedulePreview({ entries, onShowTrace, -}: { +}: Readonly<{ entries: ScheduleEntryDto[] onShowTrace: (entryId: string) => void -}) { +}>) { const { t } = useTranslation() if (entries.length === 0) return

{t('admin.channels.noSchedule')}

diff --git a/frontend/src/features/admin/channels/components/SettingsCard.tsx b/frontend/src/features/admin/channels/components/SettingsCard.tsx index 00a6c7b..417b9e5 100644 --- a/frontend/src/features/admin/channels/components/SettingsCard.tsx +++ b/frontend/src/features/admin/channels/components/SettingsCard.tsx @@ -16,13 +16,13 @@ export function SettingsCard({ bare, onSaved, onError, -}: { +}: Readonly<{ channel: ChannelDto readyAssets: { id: string; originalFileName: string }[] bare?: boolean onSaved: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [name, setName] = useState(channel.name) const [isEnabled, setIsEnabled] = useState(channel.isEnabled) diff --git a/frontend/src/features/admin/channels/components/SlotInspector.tsx b/frontend/src/features/admin/channels/components/SlotInspector.tsx index 3baee36..116b45e 100644 --- a/frontend/src/features/admin/channels/components/SlotInspector.tsx +++ b/frontend/src/features/admin/channels/components/SlotInspector.tsx @@ -68,12 +68,12 @@ export function SlotInspector({ draft, onClose, onChanged, -}: { +}: Readonly<{ channelId: string draft: SlotDraft onClose: () => void onChanged: () => void -}) { +}>) { const { t } = useTranslation() const [body, setBody] = useState(() => draft.slot ? toSlotBody(draft.slot) : emptyBody(draft.defaults), diff --git a/frontend/src/features/admin/channels/components/TemplateIssues.tsx b/frontend/src/features/admin/channels/components/TemplateIssues.tsx index c769f0a..34eb50f 100644 --- a/frontend/src/features/admin/channels/components/TemplateIssues.tsx +++ b/frontend/src/features/admin/channels/components/TemplateIssues.tsx @@ -14,11 +14,11 @@ export function TemplateIssues({ channelId, slotsById, onGoToSlot, -}: { +}: Readonly<{ channelId: string slotsById: Map onGoToSlot: (slot: SlotDto) => void -}) { +}>) { const { t } = useTranslation() const { data: issues } = useQuery({ queryKey: qk.channels.issues(channelId), @@ -53,11 +53,11 @@ function IssueRow({ issue, slot, onGoToSlot, -}: { +}: Readonly<{ issue: TemplateIssueDto slot: SlotDto | undefined onGoToSlot: (slot: SlotDto) => void -}) { +}>) { const { t } = useTranslation() const Icon = issue.severity === 'Error' ? CircleAlert : AlertTriangle diff --git a/frontend/src/features/admin/channels/components/TemplatePreview.tsx b/frontend/src/features/admin/channels/components/TemplatePreview.tsx index efdbf68..e854142 100644 --- a/frontend/src/features/admin/channels/components/TemplatePreview.tsx +++ b/frontend/src/features/admin/channels/components/TemplatePreview.tsx @@ -27,7 +27,7 @@ const PROGRAMME_KINDS = new Set(['Program', 'Fallback', 'SignOf * Предпросмотр (см. 6.4): прогон генератора по текущим правилам без записи и без продвижения * курсоров. Заставки приходят резервом известной длины — реальный рендер только при применении. */ -export function TemplatePreview({ channelId }: { channelId: string }) { +export function TemplatePreview({ channelId }: Readonly<{ channelId: string }>) { const { t } = useTranslation() const [open, setOpen] = useState(false) const [days, setDays] = useState(1) @@ -97,7 +97,7 @@ export function TemplatePreview({ channelId }: { channelId: string }) { ) } -function Programme({ preview }: { preview: SchedulePreviewDto }) { +function Programme({ preview }: Readonly<{ preview: SchedulePreviewDto }>) { const { t } = useTranslation() const items = preview.items.filter((i) => PROGRAMME_KINDS.has(i.kind)) @@ -143,7 +143,7 @@ function loadByHour(preview: SchedulePreviewDto): { hour: Date; minutes: number .map(([hour, minutes]) => ({ hour: new Date(hour), minutes })) } -function Tape({ preview }: { preview: SchedulePreviewDto }) { +function Tape({ preview }: Readonly<{ preview: SchedulePreviewDto }>) { const { t } = useTranslation() const load = useMemo(() => loadByHour(preview), [preview]) const peak = Math.max(1, ...load.map((l) => l.minutes)) @@ -180,7 +180,10 @@ function Tape({ preview }: { preview: SchedulePreviewDto }) { ) } -function TapeRow({ item, preview }: { item: PreviewItemDto; preview: SchedulePreviewDto }) { +function TapeRow({ + item, + preview, +}: Readonly<{ item: PreviewItemDto; preview: SchedulePreviewDto }>) { const { t } = useTranslation() const minutes = (new Date(item.endsAtUtc).getTime() - new Date(item.startsAtUtc).getTime()) / 60_000 @@ -200,7 +203,7 @@ function TapeRow({ item, preview }: { item: PreviewItemDto; preview: SchedulePre } /** Предупреждения, сгруппированные по виду: десять однотипных строк читаются как одна проблема. */ -function Problems({ preview }: { preview: SchedulePreviewDto }) { +function Problems({ preview }: Readonly<{ preview: SchedulePreviewDto }>) { const { t } = useTranslation() // Ключ строки — «вид + порядковый номер внутри вида»: у предупреждения нет своего id, а текст // повторяется (одна и та же причина на разных слотах), и позиция здесь — единственное, что его @@ -250,7 +253,7 @@ function Problems({ preview }: { preview: SchedulePreviewDto }) { * Тепловая карта повторов: матрица «шоу × вещательные сутки», яркость — число показов. Сразу видно, * что один фильм крутится четыре раза за неделю. */ -function RepeatHeatmap({ preview }: { preview: SchedulePreviewDto }) { +function RepeatHeatmap({ preview }: Readonly<{ preview: SchedulePreviewDto }>) { const { t } = useTranslation() const { days, rows } = useMemo(() => { diff --git a/frontend/src/features/admin/channels/components/ViewerCard.tsx b/frontend/src/features/admin/channels/components/ViewerCard.tsx index 3a3c1c2..bed76ab 100644 --- a/frontend/src/features/admin/channels/components/ViewerCard.tsx +++ b/frontend/src/features/admin/channels/components/ViewerCard.tsx @@ -21,12 +21,12 @@ export function ViewerCard({ bare, onSaved, onError, -}: { +}: Readonly<{ channel: ChannelDto bare?: boolean onSaved: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [viewer, setViewer] = useState(channel.viewer) const [galleryOpen, setGalleryOpen] = useState(false) diff --git a/frontend/src/features/admin/collections/CollectionDetail.tsx b/frontend/src/features/admin/collections/CollectionDetail.tsx index fef2f71..db41172 100644 --- a/frontend/src/features/admin/collections/CollectionDetail.tsx +++ b/frontend/src/features/admin/collections/CollectionDetail.tsx @@ -22,7 +22,7 @@ import { updateCollection, } from './api' -export function CollectionDetail({ collectionId }: { collectionId: string }) { +export function CollectionDetail({ collectionId }: Readonly<{ collectionId: string }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [galleryOpen, setGalleryOpen] = useState(false) diff --git a/frontend/src/features/admin/genres/GenresPanel.tsx b/frontend/src/features/admin/genres/GenresPanel.tsx index eaa7fb1..34e04f1 100644 --- a/frontend/src/features/admin/genres/GenresPanel.tsx +++ b/frontend/src/features/admin/genres/GenresPanel.tsx @@ -73,12 +73,12 @@ export function GenresPanel() { mutationFn: ({ id, ...body - }: { + }: Readonly<{ id: string name: string sortOrder: number aliases: string[] - }) => updateGenre(id, body), + }>) => updateGenre(id, body), onSuccess: invalidate, }) const deleteMutation = useMutation({ diff --git a/frontend/src/features/admin/groups/DurationLabel.tsx b/frontend/src/features/admin/groups/DurationLabel.tsx index 8f93b79..1803b9d 100644 --- a/frontend/src/features/admin/groups/DurationLabel.tsx +++ b/frontend/src/features/admin/groups/DurationLabel.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { splitDuration } from './format' /** Объём эфира: «118 ч 40 мин». Ноль — прочерк, потому что «0 ч» читается как сбой подсчёта. */ -export function DurationLabel({ seconds }: { seconds: number }) { +export function DurationLabel({ seconds }: Readonly<{ seconds: number }>) { const { t } = useTranslation() const parts = splitDuration(seconds) if (!parts) return <>— diff --git a/frontend/src/features/admin/groups/GroupDetail.tsx b/frontend/src/features/admin/groups/GroupDetail.tsx index 12be56a..67abef4 100644 --- a/frontend/src/features/admin/groups/GroupDetail.tsx +++ b/frontend/src/features/admin/groups/GroupDetail.tsx @@ -25,7 +25,7 @@ import { GroupFilterPanel } from './GroupFilterPanel' const EMPTY_FILTER: GroupFilter = {} -export function GroupDetail({ groupId }: { groupId: string }) { +export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) { const { t } = useTranslation() const queryClient = useQueryClient() @@ -88,7 +88,7 @@ export function GroupDetail({ groupId }: { groupId: string }) { onError, }) const weightMutation = useMutation({ - mutationFn: ({ itemId, weight }: { itemId: string; weight: number }) => + mutationFn: ({ itemId, weight }: Readonly<{ itemId: string; weight: number }>) => setGroupItemWeight(groupId, itemId, weight), onSuccess: invalidate, onError, diff --git a/frontend/src/features/admin/groups/GroupFilterPanel.tsx b/frontend/src/features/admin/groups/GroupFilterPanel.tsx index 58134ce..dccc6e0 100644 --- a/frontend/src/features/admin/groups/GroupFilterPanel.tsx +++ b/frontend/src/features/admin/groups/GroupFilterPanel.tsx @@ -14,10 +14,10 @@ const SHOW_KINDS: ShowKind[] = ['Series', 'Single'] export function GroupFilterPanel({ filter, onChange, -}: { +}: Readonly<{ filter: GroupFilter onChange: (next: GroupFilter) => void -}) { +}>) { const { t } = useTranslation() const { data: genres } = useQuery({ queryKey: qk.genres.all, queryFn: listGenres }) diff --git a/frontend/src/features/admin/images/ImageGallery.tsx b/frontend/src/features/admin/images/ImageGallery.tsx index 715547e..c472714 100644 --- a/frontend/src/features/admin/images/ImageGallery.tsx +++ b/frontend/src/features/admin/images/ImageGallery.tsx @@ -25,11 +25,11 @@ export function GalleryBrowser({ category = 'Library', onSelect, onClose, -}: { +}: Readonly<{ category?: ImageCategory onSelect?: (image: ImagePick) => void onClose?: () => void -}) { +}>) { const { t } = useTranslation() const queryClient = useQueryClient() const [active, setActive] = useState(category) @@ -193,12 +193,12 @@ export function ImageGallery({ onOpenChange, category, onSelect, -}: { +}: Readonly<{ open: boolean onOpenChange: (open: boolean) => void category?: ImageCategory onSelect?: (image: ImagePick) => void -}) { +}>) { const { t } = useTranslation() return ( diff --git a/frontend/src/features/admin/interstitials/BlockBuilder.tsx b/frontend/src/features/admin/interstitials/BlockBuilder.tsx index b956d4d..1f71413 100644 --- a/frontend/src/features/admin/interstitials/BlockBuilder.tsx +++ b/frontend/src/features/admin/interstitials/BlockBuilder.tsx @@ -16,10 +16,10 @@ import { formatClock } from './format' export function BlockBuilder({ onSaved, onError, -}: { +}: Readonly<{ onSaved: () => void onError: (error: unknown) => void -}) { +}>) { const { t } = useTranslation() const [name, setName] = useState('') const [items, setItems] = useState([]) diff --git a/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx b/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx index 64efbc4..dd34ff1 100644 --- a/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx +++ b/frontend/src/features/admin/interstitials/ClipGroupPanel.tsx @@ -15,7 +15,7 @@ import { formatClock } from './format' * Группы роликов собираются здесь же, а не в общем редакторе групп (см. 6.7): в выбранную группу * перетаскиваются и отдельные ролики, и готовые блоки — стык умеет и то и другое. */ -export function ClipGroupPanel({ onError }: { onError: (error: unknown) => void }) { +export function ClipGroupPanel({ onError }: Readonly<{ onError: (error: unknown) => void }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [selected, setSelected] = useState('') diff --git a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx index 5310f13..a52fd1f 100644 --- a/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx +++ b/frontend/src/features/admin/interstitials/InterstitialsPanel.tsx @@ -48,7 +48,7 @@ export function InterstitialsPanel() { const onError = useApiError() const renameMutation = useMutation({ - mutationFn: ({ id, name }: { id: string; name: string }) => renameShow(id, name), + mutationFn: ({ id, name }: Readonly<{ id: string; name: string }>) => renameShow(id, name), onSuccess: () => { setRenaming(null) invalidate() diff --git a/frontend/src/features/admin/media/ManualInboxDialog.tsx b/frontend/src/features/admin/media/ManualInboxDialog.tsx index b3dbec3..e52a853 100644 --- a/frontend/src/features/admin/media/ManualInboxDialog.tsx +++ b/frontend/src/features/admin/media/ManualInboxDialog.tsx @@ -45,7 +45,7 @@ function formatSize(bytes: number): string { * Номера сезона и серии распознаются здесь же и в таком виде и уходят на сервер: что показано, * то и сохранится. */ -export function ManualInboxDialog({ onClose }: { onClose: () => void }) { +export function ManualInboxDialog({ onClose }: Readonly<{ onClose: () => void }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [selected, setSelected] = useState([]) diff --git a/frontend/src/features/admin/media/MediaPanel.tsx b/frontend/src/features/admin/media/MediaPanel.tsx index 14be1f4..b78b7b7 100644 --- a/frontend/src/features/admin/media/MediaPanel.tsx +++ b/frontend/src/features/admin/media/MediaPanel.tsx @@ -261,7 +261,7 @@ export function MediaPanel() { ) } -function MediaRow({ asset, onDelete }: { asset: MediaAssetDto; onDelete: () => void }) { +function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete: () => void }>) { const { t } = useTranslation() return ( diff --git a/frontend/src/features/admin/media/UploadSnackbar.tsx b/frontend/src/features/admin/media/UploadSnackbar.tsx index a659654..915fd49 100644 --- a/frontend/src/features/admin/media/UploadSnackbar.tsx +++ b/frontend/src/features/admin/media/UploadSnackbar.tsx @@ -3,7 +3,7 @@ import { AlertCircle, Check, ChevronDown, ChevronUp, Clock, Loader2, RotateCw, X import { cn } from '@/shared/lib/cn' import { type UploadItem, useUploadStore } from './upload-store' -function StatusIcon({ status }: { status: UploadItem['status'] }) { +function StatusIcon({ status }: Readonly<{ status: UploadItem['status'] }>) { switch (status) { case 'done': return diff --git a/frontend/src/features/admin/media/UploadToShowDialog.tsx b/frontend/src/features/admin/media/UploadToShowDialog.tsx index d8a2c9c..980e052 100644 --- a/frontend/src/features/admin/media/UploadToShowDialog.tsx +++ b/frontend/src/features/admin/media/UploadToShowDialog.tsx @@ -23,7 +23,10 @@ import { useUploadStore } from './upload-store' /** Radix Select запрещает пустое значение — под «в библиотеку» используем спец-значение. */ const LIBRARY_VALUE = '__library__' -export function UploadToShowDialog({ files, onClose }: { files: File[]; onClose: () => void }) { +export function UploadToShowDialog({ + files, + onClose, +}: Readonly<{ files: File[]; onClose: () => void }>) { const { t } = useTranslation() const enqueue = useUploadStore((s) => s.enqueue) const [seasonStr, setSeasonStr] = useState('') diff --git a/frontend/src/features/admin/roles/RolesPanel.tsx b/frontend/src/features/admin/roles/RolesPanel.tsx index 8629dbd..c80e1bb 100644 --- a/frontend/src/features/admin/roles/RolesPanel.tsx +++ b/frontend/src/features/admin/roles/RolesPanel.tsx @@ -50,7 +50,7 @@ export function RolesPanel() { }) const renameMutation = useMutation({ - mutationFn: ({ id, name }: { id: string; name: string }) => updateRole(id, name), + mutationFn: ({ id, name }: Readonly<{ id: string; name: string }>) => updateRole(id, name), onSuccess: invalidate, onError, }) diff --git a/frontend/src/features/admin/shows/ShowDetail.tsx b/frontend/src/features/admin/shows/ShowDetail.tsx index 9c2dea2..4fe5815 100644 --- a/frontend/src/features/admin/shows/ShowDetail.tsx +++ b/frontend/src/features/admin/shows/ShowDetail.tsx @@ -44,7 +44,7 @@ function addButtonLabel( return `${t('admin.shows.addSelected')} (${count})` } -export function ShowDetail({ showId }: { showId: string }) { +export function ShowDetail({ showId }: Readonly<{ showId: string }>) { const { t } = useTranslation() const queryClient = useQueryClient() const [filter, setFilter] = useState('') diff --git a/frontend/src/features/admin/shows/ShowGenresField.tsx b/frontend/src/features/admin/shows/ShowGenresField.tsx index 33ef482..6985ba9 100644 --- a/frontend/src/features/admin/shows/ShowGenresField.tsx +++ b/frontend/src/features/admin/shows/ShowGenresField.tsx @@ -15,7 +15,10 @@ import { setShowGenres } from './api' * Жанры шоу: бейджи в шапке карточки + диалог правки. Основной жанр отмечается отдельно — * он показывается в списке шоу и участвует в отборе контента наравне с остальными. */ -export function ShowGenresField({ show, onChanged }: { show: ShowDto; onChanged: () => void }) { +export function ShowGenresField({ + show, + onChanged, +}: Readonly<{ show: ShowDto; onChanged: () => void }>) { const { t } = useTranslation() const [open, setOpen] = useState(false) const [selected, setSelected] = useState([]) diff --git a/frontend/src/features/admin/shows/ShowMetadataCard.tsx b/frontend/src/features/admin/shows/ShowMetadataCard.tsx index e5afa4b..afb0e3b 100644 --- a/frontend/src/features/admin/shows/ShowMetadataCard.tsx +++ b/frontend/src/features/admin/shows/ShowMetadataCard.tsx @@ -28,7 +28,10 @@ import { updateMetadata, } from './api' -export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged: () => void }) { +export function ShowMetadataCard({ + show, + onChanged, +}: Readonly<{ show: ShowDto; onChanged: () => void }>) { const { t } = useTranslation() const [galleryOpen, setGalleryOpen] = useState(false) const [provider, setProvider] = useState('') @@ -359,7 +362,7 @@ export function ShowMetadataCard({ show, onChanged }: { show: ShowDto; onChanged } /** Итог по сезону: чего не хватает — или что полный состав сезона неизвестен. */ -function SeasonGapNote({ gap }: { gap: MissingEpisodesReport['seasons'][number] }) { +function SeasonGapNote({ gap }: Readonly<{ gap: MissingEpisodesReport['seasons'][number] }>) { const { t } = useTranslation() if (gap.expected == null) diff --git a/frontend/src/features/admin/users/UsersPanel.tsx b/frontend/src/features/admin/users/UsersPanel.tsx index c550f22..14fe914 100644 --- a/frontend/src/features/admin/users/UsersPanel.tsx +++ b/frontend/src/features/admin/users/UsersPanel.tsx @@ -64,7 +64,7 @@ export function UsersPanel() { const unblockMutation = useMutation({ mutationFn: unblockUser, onSuccess: invalidate, onError }) const deleteMutation = useMutation({ mutationFn: deleteUser, onSuccess: invalidate, onError }) const changeRoleMutation = useMutation({ - mutationFn: ({ userId, roleId: nextRoleId }: { userId: string; roleId: string }) => + mutationFn: ({ userId, roleId: nextRoleId }: Readonly<{ userId: string; roleId: string }>) => changeUserRole(userId, nextRoleId), onSuccess: invalidate, onError, @@ -302,11 +302,11 @@ function ResetPasswordDialog({ user, onClose, onError, -}: { +}: Readonly<{ user: UserSummaryDto onClose: () => void onError: (e: unknown) => void -}) { +}>) { const { t } = useTranslation() const [password, setPassword] = useState('') diff --git a/frontend/src/features/auth/LoginForm.tsx b/frontend/src/features/auth/LoginForm.tsx index 5bb53fe..2f88365 100644 --- a/frontend/src/features/auth/LoginForm.tsx +++ b/frontend/src/features/auth/LoginForm.tsx @@ -16,7 +16,7 @@ const schema = z.object({ type FormValues = z.infer -export function LoginForm({ onSuccess }: { onSuccess: () => void }) { +export function LoginForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) { const { t } = useTranslation() const { register: registerField, diff --git a/frontend/src/features/auth/RegisterForm.tsx b/frontend/src/features/auth/RegisterForm.tsx index 817df16..b2c9ceb 100644 --- a/frontend/src/features/auth/RegisterForm.tsx +++ b/frontend/src/features/auth/RegisterForm.tsx @@ -16,7 +16,7 @@ const schema = z.object({ type FormValues = z.infer -export function RegisterForm({ onSuccess }: { onSuccess: () => void }) { +export function RegisterForm({ onSuccess }: Readonly<{ onSuccess: () => void }>) { const { t } = useTranslation() const { register: registerField, diff --git a/frontend/src/features/streaming/AirPage.tsx b/frontend/src/features/streaming/AirPage.tsx index 95a71db..c50b50d 100644 --- a/frontend/src/features/streaming/AirPage.tsx +++ b/frontend/src/features/streaming/AirPage.tsx @@ -15,7 +15,7 @@ function formatTime(iso: string) { } /** Кадр серии, если он есть; иначе постер шоу (он вертикальный, отсюда другая ширина). */ -function EntryThumb({ entry }: { entry: PublicEpgEntryDto | undefined }) { +function EntryThumb({ entry }: Readonly<{ entry: PublicEpgEntryDto | undefined }>) { if (entry?.episodeStillImageId) return ( void -}) { +}>) { const { t } = useTranslation() const containerRef = useRef(null) const videoRef = useRef(null) diff --git a/frontend/src/features/streaming/PlayerOverlays.tsx b/frontend/src/features/streaming/PlayerOverlays.tsx index 65885fb..2f1442d 100644 --- a/frontend/src/features/streaming/PlayerOverlays.tsx +++ b/frontend/src/features/streaming/PlayerOverlays.tsx @@ -19,11 +19,11 @@ export function ChannelLogo({ imageId, corner, opacity, -}: { +}: Readonly<{ imageId: string corner: LogoCorner opacity: number -}) { +}>) { return ( ) { const { t } = useTranslation() return ( @@ -65,7 +65,7 @@ export function NextUpBanner({ title }: { title: string }) { * Аналоговый фильтр: лёгкий VHS-шум, дрожание и размытие краёв. Переборщить очень легко, поэтому * сила регулируется, а вклад каждого слоя от неё убывает нелинейно. */ -export function AnalogFilter({ strength }: { strength: number }) { +export function AnalogFilter({ strength }: Readonly<{ strength: number }>) { const s = Math.min(1, Math.max(0, strength)) return ( <> @@ -91,7 +91,7 @@ export function AnalogFilter({ strength }: { strength: number }) { } /** Короткий чёрный кадр с номером канала — как при переключении на телевизоре. */ -export function ChannelFlash({ number, name }: { number: number | null; name: string }) { +export function ChannelFlash({ number, name }: Readonly<{ number: number | null; name: string }>) { return (
diff --git a/frontend/src/shared/ui/ToastProvider.tsx b/frontend/src/shared/ui/ToastProvider.tsx index cefdbf9..8bdb4e2 100644 --- a/frontend/src/shared/ui/ToastProvider.tsx +++ b/frontend/src/shared/ui/ToastProvider.tsx @@ -8,7 +8,7 @@ import { let nextId = 1 -export function ToastProvider({ children }: { children: ReactNode }) { +export function ToastProvider({ children }: Readonly<{ children: ReactNode }>) { const [toasts, setToasts] = useState([]) const push = useCallback((message: string, variant: ToastVariant) => { diff --git a/frontend/src/shared/ui/badge.tsx b/frontend/src/shared/ui/badge.tsx index 14d0f8e..0b953c2 100644 --- a/frontend/src/shared/ui/badge.tsx +++ b/frontend/src/shared/ui/badge.tsx @@ -18,6 +18,6 @@ const badgeVariants = cva( export type BadgeProps = HTMLAttributes & VariantProps -export function Badge({ className, variant, ...props }: BadgeProps) { +export function Badge({ className, variant, ...props }: Readonly) { return } diff --git a/frontend/src/shared/ui/hls-video.tsx b/frontend/src/shared/ui/hls-video.tsx index 8e77cfd..48e596d 100644 --- a/frontend/src/shared/ui/hls-video.tsx +++ b/frontend/src/shared/ui/hls-video.tsx @@ -7,7 +7,7 @@ import { cn } from '@/shared/lib/cn' * Мини-плеер HLS для админки: плейлист и сегменты лежат под admin-роутами (JWT), поэтому запросы * идут через hls.js с Bearer-заголовком. Нативный путь (Safari) — только там, где hls.js не нужен. */ -export function HlsVideo({ src, className }: { src: string; className?: string }) { +export function HlsVideo({ src, className }: Readonly<{ src: string; className?: string }>) { const videoRef = useRef(null) useEffect(() => { diff --git a/frontend/src/shared/ui/pager.tsx b/frontend/src/shared/ui/pager.tsx index 871ba7d..ac48f3b 100644 --- a/frontend/src/shared/ui/pager.tsx +++ b/frontend/src/shared/ui/pager.tsx @@ -6,11 +6,11 @@ export function Pager({ page, totalPages, onChange, -}: { +}: Readonly<{ page: number totalPages: number onChange: (page: number) => void -}) { +}>) { const { t } = useTranslation() if (totalPages <= 1) return null diff --git a/frontend/src/shared/ui/sortable.tsx b/frontend/src/shared/ui/sortable.tsx index 52ef439..aa13c99 100644 --- a/frontend/src/shared/ui/sortable.tsx +++ b/frontend/src/shared/ui/sortable.tsx @@ -9,13 +9,13 @@ export function SortHeader({ sort, onToggle, className, -}: { +}: Readonly<{ label: string sortKey: string sort: SortState onToggle: (key: string) => void className?: string -}) { +}>) { const active = sort.key === sortKey const direction = sort.desc ? 'descending' : 'ascending' let Icon = ChevronsUpDown diff --git a/frontend/src/shared/ui/toaster.tsx b/frontend/src/shared/ui/toaster.tsx index 2907ea1..fb65715 100644 --- a/frontend/src/shared/ui/toaster.tsx +++ b/frontend/src/shared/ui/toaster.tsx @@ -21,12 +21,12 @@ function ToastItem({ message, variant, onDismiss, -}: { +}: Readonly<{ id: number message: string variant: 'default' | 'success' | 'error' onDismiss: (id: number) => void -}) { +}>) { const { t } = useTranslation() useEffect(() => { diff --git a/frontend/src/theme/ThemeProvider.tsx b/frontend/src/theme/ThemeProvider.tsx index f69ab0b..effa6fb 100644 --- a/frontend/src/theme/ThemeProvider.tsx +++ b/frontend/src/theme/ThemeProvider.tsx @@ -13,7 +13,7 @@ function applyTheme(theme: Theme) { root.classList.toggle('dark', resolve(theme) === 'dark') } -export function ThemeProvider({ children }: { children: ReactNode }) { +export function ThemeProvider({ children }: Readonly<{ children: ReactNode }>) { const [theme, setThemeState] = useState( () => (localStorage.getItem(THEME_STORAGE_KEY) as Theme | null) ?? 'dark', )