103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { AlertTriangle } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/shared/ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/shared/ui/dialog'
|
|
import { cn } from '@/shared/lib/cn'
|
|
import { getApplyDiff } from '../api'
|
|
import { formatChannelTime } from '../lib/format'
|
|
|
|
/**
|
|
* Диф перед применением (см. 6.6). Изменения в ближайшие сутки подсвечены отдельно — это самая
|
|
* частая причина случайного ущерба: у зрителя из-под носа уезжает то, что он уже видит в программе.
|
|
*/
|
|
export function ApplyDialog({
|
|
channelId,
|
|
utcOffsetMinutes,
|
|
pending,
|
|
onApply,
|
|
onClose,
|
|
}: {
|
|
channelId: string
|
|
utcOffsetMinutes: number
|
|
pending: boolean
|
|
onApply: () => void
|
|
onClose: () => void
|
|
}) {
|
|
const { t } = useTranslation()
|
|
const { data, isFetching } = useQuery({
|
|
queryKey: ['admin', 'channels', channelId, 'diff'],
|
|
queryFn: () => getApplyDiff(channelId),
|
|
staleTime: 0,
|
|
gcTime: 0,
|
|
})
|
|
|
|
return (
|
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>{t('admin.channels.apply')}</DialogTitle>
|
|
<DialogDescription>
|
|
{isFetching || !data
|
|
? t('common.loading')
|
|
: t('admin.channels.diffSummary', { total: data.total, changed: data.changed })}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{data && (
|
|
<div className="flex flex-col gap-2 text-sm">
|
|
{data.changedSoon > 0 && (
|
|
<p className="flex items-center gap-1.5 text-amber-500">
|
|
<AlertTriangle className="h-4 w-4 shrink-0" />
|
|
{t('admin.channels.diffSoon', { count: data.changedSoon })}
|
|
</p>
|
|
)}
|
|
|
|
{data.changes.length === 0 ? (
|
|
<p className="text-muted-foreground">{t('admin.channels.diffNoChanges')}</p>
|
|
) : (
|
|
<ul className="max-h-80 divide-y divide-border overflow-y-auto text-xs">
|
|
{data.changes.map((change, index) => (
|
|
<li
|
|
key={`${change.startsAtUtc}-${index}`}
|
|
className={cn('flex items-center gap-2 py-1', change.soon && 'text-amber-500')}
|
|
>
|
|
<span className="w-24 shrink-0 tabular-nums text-muted-foreground">
|
|
{formatChannelTime(change.startsAtUtc, utcOffsetMinutes)}
|
|
</span>
|
|
<span className="min-w-0 flex-1 truncate">{change.before ?? '—'}</span>
|
|
<span className="shrink-0 text-muted-foreground">→</span>
|
|
<span className="min-w-0 flex-1 truncate">{change.after ?? '—'}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{data.changed > data.changes.length && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('admin.channels.andMore', { count: data.changed - data.changes.length })}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button size="sm" variant="outline" onClick={onClose}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button size="sm" disabled={pending} onClick={onApply}>
|
|
{t('admin.channels.apply')}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|