import { Anchor, Plus } from 'lucide-react' import { useTranslation } from 'react-i18next' import type { GridLayerDto, ScheduleTemplateDto, SlotDto } from '@/shared/api/types' import { Button } from '@/shared/ui/button' import { cn } from '@/shared/lib/cn' const HOUR_HEIGHT = 44 const WEEKDAYS = [1, 2, 3, 4, 5, 6, 0] /** Цвет блока — по дейпарту: сетка должна читаться одним взглядом, без легенды. */ const DAYPART_CLASS: Record = { Morning: 'bg-amber-500/20 border-amber-500/40', Day: 'bg-sky-500/20 border-sky-500/40', Prime: 'bg-violet-500/25 border-violet-500/50', Night: 'bg-slate-500/20 border-slate-500/40', } function minutesOf(time: string): number { const [h, m] = time.split(':') return Number(h) * 60 + Number(m) } /** * Смещение слота от начала вещательных суток. Ночной блок (00:00–06:00 при старте суток в 06:00) * принадлежит предыдущему дню, поэтому его смещение больше суточного, а не отрицательное. */ function offsetInDay(slotStart: string, dayStart: string): number { const diff = minutesOf(slotStart) - minutesOf(dayStart) return diff >= 0 ? diff : diff + 24 * 60 } /** Слоты, попадающие в колонку дня: слот без дня недели идёт каждый день. */ function slotsOfDay(layers: GridLayerDto[], weekday: number) { return layers .filter((layer) => layer.isEnabled) .flatMap((layer) => layer.slots .filter((slot) => slot.weekday === null || slot.weekday === weekday) .map((slot) => ({ slot, layer })), ) } export function ScheduleGrid({ template, selectedSlotId, onSelectSlot, onAddSlot, }: { template: ScheduleTemplateDto selectedSlotId: string | null onSelectSlot: (slot: SlotDto) => void onAddSlot: (weekday: number, startMinutes: number) => void }) { const { t } = useTranslation() const dayStart = template.dayStartTime.slice(0, 5) const dayStartMinutes = minutesOf(dayStart) // Подписи часов идут от начала вещательных суток, а не от полуночи. const hours = Array.from({ length: 24 }, (_, i) => (dayStartMinutes / 60 + i) % 24) // Слои отсортированы по убыванию приоритета: слот, лежащий под более приоритетным, штрихуем. const ordered = [...template.layers].sort((a, b) => b.priority - a.priority) const isCovered = (slot: SlotDto, layer: GridLayerDto, weekday: number) => { const from = offsetInDay(slot.targetStart, dayStart) const to = from + slot.targetDurationMinutes return ordered .filter((other) => other.isEnabled && other.priority > layer.priority) .some((other) => other.slots .filter((s) => s.weekday === null || s.weekday === weekday) .some((s) => { const otherFrom = offsetInDay(s.targetStart, dayStart) return from < otherFrom + s.targetDurationMinutes && otherFrom < to }), ) } return (
{dayStart}
{WEEKDAYS.map((weekday) => (
{t(`admin.channels.weekdays.${weekday}`)}
))}
{hours.map((hour, index) => (
{hour.toString().padStart(2, '0')}:00
))}
{WEEKDAYS.map((weekday) => (
{hours.map((_, index) => ( ))} {slotsOfDay(ordered, weekday).map(({ slot, layer }) => { const from = offsetInDay(slot.targetStart, dayStart) const covered = isCovered(slot, layer, weekday) return ( ) })}
))}
) } /** Панель слоёв: видимость, приоритет и выбор редактируемого. */ export function LayerList({ template, activeLayerId, onSelect, onDelete, }: { template: ScheduleTemplateDto activeLayerId: string | null onSelect: (layer: GridLayerDto) => void onDelete: (layer: GridLayerDto) => void }) { const { t } = useTranslation() const ordered = [...template.layers].sort((a, b) => b.priority - a.priority) return ( ) }