Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { Tag } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { listGenres } from '@/features/admin/genres/api'
|
||||
import { HttpError } from '@/shared/api/client'
|
||||
import type { ShowDto } from '@/shared/api/types'
|
||||
import { Badge } from '@/shared/ui/badge'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/shared/ui/dialog'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { setShowGenres } from './api'
|
||||
|
||||
/**
|
||||
* Жанры шоу: бейджи в шапке карточки + диалог правки. Основной жанр отмечается отдельно —
|
||||
* он показывается в списке шоу и участвует в отборе контента наравне с остальными.
|
||||
*/
|
||||
export function ShowGenresField({ show, onChanged }: { show: ShowDto; onChanged: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selected, setSelected] = useState<string[]>([])
|
||||
const [primary, setPrimary] = useState<string | null>(null)
|
||||
|
||||
const { data: genres } = useQuery({
|
||||
queryKey: ['admin', 'genres'],
|
||||
queryFn: listGenres,
|
||||
enabled: open,
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => setShowGenres(show.id, selected, primary),
|
||||
onSuccess: () => {
|
||||
onChanged()
|
||||
setOpen(false)
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof HttpError ? error.detail : t('common.error')),
|
||||
})
|
||||
|
||||
const openDialog = () => {
|
||||
setSelected(show.genres.map((g) => g.id))
|
||||
setPrimary(show.genres.find((g) => g.isPrimary)?.id ?? null)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const toggle = (id: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
// Снятый жанр не может остаться основным — иначе сервер молча выберет другой.
|
||||
if (!next.includes(id) && primary === id) setPrimary(next[0] ?? null)
|
||||
if (next.includes(id) && primary === null) setPrimary(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{show.genres.length === 0 ? (
|
||||
<span className="text-sm text-muted-foreground">{t('admin.shows.genresEmpty')}</span>
|
||||
) : (
|
||||
show.genres.map((genre) => (
|
||||
<Badge key={genre.id} variant={genre.isPrimary ? 'default' : 'muted'}>
|
||||
{genre.name}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={openDialog}>
|
||||
<Tag className="h-4 w-4" /> {t('admin.shows.genresEdit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.shows.genresEdit')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-xs text-muted-foreground">{t('admin.shows.genresHint')}</p>
|
||||
<div className="flex max-h-80 flex-col gap-1 overflow-y-auto">
|
||||
{(genres ?? []).map((genre) => {
|
||||
const checked = selected.includes(genre.id)
|
||||
return (
|
||||
<div
|
||||
key={genre.id}
|
||||
className="flex items-center justify-between gap-3 rounded px-2 py-1 hover:bg-muted/40"
|
||||
>
|
||||
<label className="flex flex-1 items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={checked} onChange={() => toggle(genre.id)} />
|
||||
{genre.name}
|
||||
</label>
|
||||
{checked && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<input
|
||||
type="radio"
|
||||
name="primaryGenre"
|
||||
checked={primary === genre.id}
|
||||
onChange={() => setPrimary(genre.id)}
|
||||
/>
|
||||
{t('admin.shows.genrePrimary')}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button disabled={mutation.isPending} onClick={() => mutation.mutate()}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user