Refactor ChannelEndpoints and ScheduleGenerator: consolidate endpoint logic into partial files, remove unused methods, and enhance dependency injection for scheduling. Update ChannelDetail component to streamline imports and improve UI structure.
build / backend (push) Successful in 2m17s
build / frontend (push) Successful in 52s
tests / backend-tests (push) Successful in 2m38s

This commit is contained in:
Leonid Pershin
2026-07-25 20:53:52 +03:00
parent 8484587313
commit 5bc6e144d2
24 changed files with 2546 additions and 2431 deletions
@@ -0,0 +1,181 @@
import { useMutation } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { BumperFont, BumperSelection, BumperSettings, ChannelDto } from '@/shared/api/types'
import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { toast } from '@/shared/ui/toast-store'
import { addBumperTemplate, updateChannelSettings } from '../api'
import { clampChance } from '../lib/format'
import { BumperTemplateEditor } from './BumperTemplateEditor'
import { CollapsibleCard } from './CollapsibleCard'
export function BumperCard({
channel,
onSaved,
onError,
}: {
channel: ChannelDto
onSaved: () => void
onError: (e: unknown) => void
}) {
const { t } = useTranslation()
const [bumpersEnabled, setBumpersEnabled] = useState(channel.bumpersEnabled)
const [bumper, setBumper] = useState<BumperSettings>(channel.bumper)
const setField = <K extends keyof BumperSettings>(key: K, value: BumperSettings[K]) =>
setBumper((prev) => ({ ...prev, [key]: value }))
useEffect(() => {
setBumpersEnabled(channel.bumpersEnabled)
setBumper(channel.bumper)
}, [channel])
// Общие настройки заставок сохраняются тем же эндпоинтом, что и настройки канала — остальные
// поля берём из канала без изменений (они правятся в своей карточке).
const save = useMutation({
mutationFn: () =>
updateChannelSettings(channel.id, {
name: channel.name,
isEnabled: channel.isEnabled,
adInsertion: channel.adInsertion,
adsPerBreak: channel.adsPerBreak,
bumpersEnabled,
bumper,
fillerAssetId: channel.fillerAssetId,
}),
onSuccess: () => {
toast.success(t('settings.saved'))
onSaved()
},
onError,
})
const addTemplate = useMutation({
mutationFn: () => addBumperTemplate(channel.id, ''),
onSuccess: onSaved,
onError,
})
const templates = [...channel.bumperTemplates].sort((a, b) => a.position - b.position)
return (
<CollapsibleCard title={t('admin.channels.bumpers')} contentClassName="flex flex-col gap-4">
<label className="flex items-start gap-2 text-sm">
<input
type="checkbox"
className="mt-1"
checked={bumpersEnabled}
onChange={(e) => setBumpersEnabled(e.target.checked)}
/>
<span>
{t('admin.channels.bumpersLabel')}
<span className="block text-xs text-muted-foreground">
{t('admin.channels.bumpersHint')}
</span>
</span>
</label>
{/* Общие настройки */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.bumperSelection')}</Label>
<Select value={bumper.selection} onValueChange={(v) => setField('selection', v as BumperSelection)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Rotation">{t('admin.channels.bumperSelectionRotation')}</SelectItem>
<SelectItem value="Random">{t('admin.channels.bumperSelectionRandom')}</SelectItem>
<SelectItem value="WeightedRandom">
{t('admin.channels.bumperSelectionWeighted')}
</SelectItem>
<SelectItem value="AlwaysFirst">
{t('admin.channels.bumperSelectionAlwaysFirst')}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.bumperFont')}</Label>
<Select value={bumper.font} onValueChange={(v) => setField('font', v as BumperFont)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Sans">{t('admin.channels.bumperFontSans')}</SelectItem>
<SelectItem value="Serif">{t('admin.channels.bumperFontSerif')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.bumperMinInterval')}</Label>
<Input
type="number"
min={0}
max={1440}
value={bumper.minIntervalMinutes}
onChange={(e) => setField('minIntervalMinutes', Number(e.target.value))}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.bumperShowChangeChance')}</Label>
<Input
type="number"
min={0}
max={1}
step={0.05}
value={bumper.showChangeChance}
onChange={(e) => setField('showChangeChance', clampChance(e.target.value))}
/>
<span className="text-xs text-muted-foreground">
{t('admin.channels.bumperShowChangeChanceHint')}
</span>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.bumperEpisodeChangeChance')}</Label>
<Input
type="number"
min={0}
max={1}
step={0.05}
value={bumper.episodeChangeChance}
onChange={(e) => setField('episodeChangeChance', clampChance(e.target.value))}
/>
<span className="text-xs text-muted-foreground">
{t('admin.channels.bumperEpisodeChangeChanceHint')}
</span>
</div>
</div>
<div className="flex justify-end">
<Button size="sm" disabled={save.isPending} onClick={() => save.mutate()}>
{t('common.save')}
</Button>
</div>
{/* Блоки заставок */}
<div className="border-t border-border pt-4">
<p className="text-sm font-medium">{t('admin.channels.bumperTemplates')}</p>
<p className="text-xs text-muted-foreground">{t('admin.channels.bumperTemplatesHint')}</p>
</div>
<div className="flex flex-col gap-3">
{templates.map((template) => (
<BumperTemplateEditor
key={template.id}
channelId={channel.id}
template={template}
onChanged={onSaved}
onError={onError}
/>
))}
</div>
<div className="flex justify-center">
<Button size="sm" variant="outline" disabled={addTemplate.isPending} onClick={() => addTemplate.mutate()}>
{t('admin.channels.bumperAddTemplate')}
</Button>
</div>
</CollapsibleCard>
)
}