import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { FolderInput, ListPlus, Upload } from 'lucide-react' import { qk } from '@/shared/api/query-keys' import type { MediaAssetDto, MediaAssetStatus } from '@/shared/api/types' import { useApiError } from '@/shared/lib/use-api-error' import { Badge, type BadgeProps } from '@/shared/ui/badge' import { Button } from '@/shared/ui/button' import { Pager } from '@/shared/ui/pager' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select' import { useTableSort } from '@/shared/lib/table-sort' import { SortHeader } from '@/shared/ui/sortable' import { deleteMedia, getMediaStats, listMedia } from './api' import { formatDuration } from './format' import { ManualInboxDialog } from './ManualInboxDialog' import { UploadToShowDialog } from './UploadToShowDialog' import { useUploadStore } from './upload-store' const PAGE_SIZE = 20 type MediaFilter = 'active' | 'Pending' | 'Processing' | 'all' | 'Ready' | 'Failed' const filterStatuses: Record = { active: ['Pending', 'Processing'], Pending: ['Pending'], Processing: ['Processing'], all: [], Ready: ['Ready'], Failed: ['Failed'], } const statusVariant: Record = { Ready: 'default', Processing: 'muted', Pending: 'muted', Failed: 'destructive', } export function MediaPanel() { const { t } = useTranslation() const queryClient = useQueryClient() const fileInput = useRef(null) const fileInputShow = useRef(null) const [filter, setFilter] = useState('active') const [page, setPage] = useState(1) const { sort, toggle } = useTableSort('created', true) const [filesForShow, setFilesForShow] = useState(null) const [manualOpen, setManualOpen] = useState(false) const enqueue = useUploadStore((s) => s.enqueue) const sortColumn = (key: string) => { setPage(1) toggle(key) } const { data, isLoading, refetch } = useQuery({ queryKey: qk.media.list(filter, page, sort.key, sort.desc), queryFn: () => listMedia({ page, pageSize: PAGE_SIZE, statuses: filterStatuses[filter], sort: sort.key, desc: sort.desc, }), // Пока что-то обрабатывается — обновляем чаще, чтобы статус ехал в UI. refetchInterval: (query) => query.state.data?.items.some((a) => a.status === 'Processing' || a.status === 'Pending') ? 4000 : false, }) const { data: stats } = useQuery({ queryKey: qk.media.stats, queryFn: getMediaStats, // Пока есть незавершённая работа — освежаем чипы очереди/обработки. refetchInterval: (query) => (query.state.data?.queued ?? 0) + (query.state.data?.processing ?? 0) > 0 ? 4000 : 15000, }) /** * Сводка очереди опрашивается всегда, и она же служит датчиком: изменилась пара * «в очереди / в обработке» — значит какая-то запись сменила статус, и список пора перечитать. * Без этого список обновлялся бы, только пока активные записи видны на текущей странице: при * фильтре «Готовые» или пустом списке опрос выключался, и новый файл из inbox не появлялся. */ const activity = `${stats?.queued ?? 0}:${stats?.processing ?? 0}` const lastActivity = useRef(activity) useEffect(() => { if (lastActivity.current === activity) return lastActivity.current = activity void refetch() }, [activity, refetch]) const invalidate = () => queryClient.invalidateQueries({ queryKey: qk.media.all }) const onError = useApiError() const deleteMutation = useMutation({ mutationFn: deleteMedia, onSuccess: invalidate, onError }) return (

{t('admin.media.title')}

{stats && (
{t('admin.media.stats.queuedShort')}:{' '} {stats.queued} {t('admin.media.stats.processingShort')}:{' '} {stats.processing} {t('admin.media.stats.averageShort')}:{' '} {formatDuration(stats.averageProcessingSeconds)}
)}
{ const files = e.target.files if (files && files.length > 0) void enqueue(Array.from(files)) e.target.value = '' }} /> { const files = e.target.files if (files && files.length > 0) setFilesForShow(Array.from(files)) e.target.value = '' }} />
{filesForShow && ( setFilesForShow(null)} /> )} {manualOpen && setManualOpen(false)} />}
{isLoading && ( )} {data?.items.map((asset) => ( deleteMutation.mutate(asset.id)} /> ))} {data && data.items.length === 0 && !isLoading && ( )}
{t('common.actions')}
{t('common.loading')}
{t('admin.media.empty')}
) } function MediaRow({ asset, onDelete }: Readonly<{ asset: MediaAssetDto; onDelete: () => void }>) { const { t } = useTranslation() return ( {asset.originalFileName} {t(`admin.media.statuses.${asset.status}`)} {formatDuration(asset.durationSeconds)} {asset.width && asset.height ? `${asset.width}×${asset.height}` : '—'} {asset.status === 'Ready' ? formatDuration(asset.processingSeconds) : '—'} ) }