Загрузка данных


(function() {
    'use strict';

    // 1. Константы палитры
    const PALETTE = [
        '#a8c7fa', '#f2b8b5', '#fde293', '#a8dab5',
        '#fcc7a5', '#a5e3e0', '#d7aefb', '#d3bfb2'
    ];

    // 2. Вспомогательные функции парсинга времени и длительности
    function parseTime(val) {
        if (typeof val !== 'string') return { error: 'unparsable', value: val };
        let str = val.trim().toLowerCase();
        
        // Замена О на 0 (кириллица и латиница)
        str = str.replace(/[оo]/g, '0');
        // Замена точки на двоеточие
        str = str.replace(/\./g, ':');
        // Удаление лишних пробелов
        str = str.replace(/\s+/g, '');

        let isPm = false;
        let isAm = false;

        // Обработка AM/PM (в любом регистре и пунктуации)
        if (str.includes('pm') || str.includes('p:m')) {
            isPm = true;
            str = str.replace(/p:m|pm/g, '');
        } else if (str.includes('am') || str.includes('a:m')) {
            isAm = true;
            str = str.replace(/a:m|am/g, '');
        }

        // Разделение на часы и минуты
        let parts = str.split(':');
        if (parts.length > 2) return { error: 'unparsable', value: val };

        let hours = parseInt(parts[0], 10);
        let minutes = parts.length === 2 ? parseInt(parts[1], 10) : 0;

        if (isNaN(hours) || isNaN(minutes)) return { error: 'unparsable', value: val };

        // Коррекция для 12-часового формата
        if (isPm && hours !== 12) hours += 12;
        if (isAm && hours === 12) hours = 0;

        if (hours < 0 || hours > 23) return { error: 'hour-out-of-range', value: val };
        if (minutes < 0 || minutes > 59) return { error: 'minute-out-of-range', value: val };

        return { hours, minutes, totalMinutes: hours * 60 + minutes };
    }

    function parseDuration(val) {
        if (typeof val !== 'string') return { error: 'unparsable', value: val };
        let str = val.trim().toLowerCase();

        // Форматы: N мин, Xч Yм, X ч, Yм
        // Убираем 'мин' и 'м', оставляем 'ч'
        str = str.replace(/мин/g, '').replace(/м/g, '').trim();
        
        let hours = 0;
        let minutes = 0;

        if (str.includes('ч')) {
            let parts = str.split('ч');
            let hPart = parts[0].trim();
            let mPart = parts[1] ? parts[1].trim() : '0';
            hours = parseInt(hPart, 10);
            minutes = parseInt(mPart, 10);
        } else {
            minutes = parseInt(str, 10);
        }

        if (isNaN(hours) || isNaN(minutes)) return { error: 'unparsable', value: val };
        if (hours < 0 || minutes < 0) return { error: 'unparsable', value: val };

        return { totalMinutes: hours * 60 + minutes };
    }

    // 3. Основная логика
    function init() {
        const input = window.INPUT;
        if (!input) return;

        const errors = [];
        const validEvents = [];
        const daysCount = input.days;
        const startDate = new Date(input.startDate + 'T00:00:00');

        // Сначала парсим и валидируем все события
        input.events.forEach(ev => {
            let isValid = true;
            let errorValue = null;
            let errorReason = null;

            // Проверка даты
            const evDate = new Date(ev.date + 'T00:00:00');
            const diffTime = Math.abs(evDate - startDate);
            const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
            
            // Проверяем, входит ли дата в период [startDate, startDate + days - 1]
            if (evDate < startDate || diffDays >= daysCount) {
                isValid = false;
                errorValue = ev.date;
                errorReason = 'date-out-of-range';
            }

            // Проверка start
            const startRes = parseTime(ev.start);
            if (isValid && startRes.error) {
                isValid = false;
                errorValue = ev.start;
                errorReason = startRes.error;
            }

            // Проверка end / duration
            let endMin = null;
            if (isValid) {
                if (ev.end) {
                    const endRes = parseTime(ev.end);
                    if (endRes.error) {
                        isValid = false;
                        errorValue = ev.end;
                        errorReason = endRes.error;
                    } else {
                        endMin = endRes.totalMinutes;
                    }
                } else if (ev.duration) {
                    const durRes = parseDuration(ev.duration);
                    if (durRes.error) {
                        isValid = false;
                        errorValue = ev.duration;
                        errorReason = durRes.error;
                    } else {
                        endMin = startRes.totalMinutes + durRes.totalMinutes;
                    }
                }
            }

            // Проверка логики времени
            if (isValid) {
                if (endMin <= startRes.totalMinutes) {
                    isValid = false;
                    errorValue = ev.end || ev.duration;
                    errorReason = 'end-before-start';
                }
            }

            if (!isValid) {
                errors.push({ id: ev.id, value: errorValue, reason: errorReason });
            } else {
                validEvents.push({
                    id: ev.id,
                    title: ev.title,
                    date: ev.date,
                    startMin: startRes.totalMinutes,
                    endMin: endMin,
                    originalIndex: input.events.indexOf(ev) // Для сортировки при равенстве
                });
            }
        });

        // Записываем ошибки в глобальную область
        window.ERRORS = errors;

        // 4. Отрисовка календаря
        renderCalendar(validEvents, input.startDate, daysCount);
    }

    function renderCalendar(events, startDateStr, daysCount) {
        const container = document.getElementById('calendar');
        if (!container) return;

        container.style.position = 'relative';
        container.style.width = (60 + daysCount * 240) + 'px';
        container.style.height = '760px';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.fontSize = '12px';
        container.style.boxSizing = 'border-box';
        container.style.backgroundColor = '#fff';

        // Сетка (вертикальные разделители дней)
        for (let i = 0; i <= daysCount; i++) {
            const line = document.createElement('div');
            line.style.position = 'absolute';
            line.style.left = (60 + i * 240) + 'px';
            line.style.top = '40px';
            line.style.width = '1px';
            line.style.height = '720px';
            line.style.backgroundColor = '#ccc';
            container.appendChild(line);
        }

        // Горизонтальные линии сетки
        for (let i = 0; i <= 24; i++) {
            const line = document.createElement('div');
            line.style.position = 'absolute';
            line.style.left = '0';
            line.style.top = (40 + i * 30) + 'px';
            line.style.width = '100%';
            line.style.height = '1px';
            line.style.backgroundColor = '#e0e0e0';
            container.appendChild(line);
        }

        // Шапка календаря
        const header = document.createElement('div');
        header.style.position = 'absolute';
        header.style.top = '0';
        header.style.left = '0';
        header.style.width = '100%';
        header.style.height = '40px';
        header.style.borderBottom = '1px solid #ccc';
        header.style.boxSizing = 'border-box';
        container.appendChild(header);

        // Заполнение шапки днями
        for (let i = 0; i < daysCount; i++) {
            const dayCell = document.createElement('div');
            dayCell.style.position = 'absolute';
            dayCell.style.left = (60 + i * 240) + 'px';
            dayCell.style.top = '0';
            dayCell.style.width = '240px';
            dayCell.style.height = '40px';
            dayCell.style.lineHeight = '40px';
            dayCell.style.textAlign = 'center';
            dayCell.style.borderLeft = '1px solid #ccc';
            dayCell.style.boxSizing = 'border-box';
            
            // Вычисляем дату для этого дня
            const d = new Date(startDateStr + 'T00:00:00');
            d.setDate(d.getDate() + i);
            const dateStr = d.toISOString().split('T')[0];
            
            // Отображаем день недели и число
            const daysOfWeek = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
            dayCell.textContent = `${daysOfWeek[d.getDay()]}, ${d.getDate()}`;
            header.appendChild(dayCell);
        }

        // Левая панель с часами
        for (let i = 0; i <= 24; i++) {
            const timeLabel = document.createElement('div');
            timeLabel.style.position = 'absolute';
            timeLabel.style.left = '0';
            timeLabel.style.top = (40 + i * 30) + 'px';
            timeLabel.style.width = '60px';
            timeLabel.style.height = '30px';
            timeLabel.style.lineHeight = '30px';
            timeLabel.style.textAlign = 'center';
            timeLabel.style.color = '#5f6368';
            timeLabel.style.fontSize = '11px';
            timeLabel.textContent = i + ':00';
            container.appendChild(timeLabel);
        }

        // --- Расчёт колонок для пересекающихся событий ---
        const eventsByDate = {};
        events.forEach(ev => {
            if (!eventsByDate[ev.date]) eventsByDate[ev.date] = [];
            eventsByDate[ev.date].push(ev);
        });

        // Сортируем события по времени начала, при равенстве — по исходному порядку
        Object.keys(eventsByDate).forEach(date => {
            const dayEvents = eventsByDate[date];
            dayEvents.sort((a, b) => {
                if (a.startMin !== b.startMin) return a.startMin - b.startMin;
                return a.originalIndex - b.originalIndex;
            });

            // Группировка пересекающихся событий
            const groups = [];
            let currentGroup = [];
            let groupEnd = -1;

            dayEvents.forEach(ev => {
                if (currentGroup.length === 0 || ev.startMin >= groupEnd) {
                    // Начинаем новую группу
                    if (currentGroup.length > 0) groups.push(currentGroup);
                    currentGroup = [ev];
                    groupEnd = ev.endMin;
                } else {
                    // Добавляем в текущую группу (пересечение)
                    currentGroup.push(ev);
                    groupEnd = Math.max(groupEnd, ev.endMin);
                }
            });
            if (currentGroup.length > 0) groups.push(currentGroup);

            // Распределение по колонкам внутри каждой группы
            groups.forEach(group => {
                const cols = [];
                group.forEach(ev => {
                    let placed = false;
                    for (let c = 0; c < cols.length; c++) {
                        // Проверяем, можно ли положить в эту колонку
                        const lastInCol = cols[c][cols[c].length - 1];
                        if (lastInCol.endMin <= ev.startMin) {
                            cols[c].push(ev);
                            ev.colIndex = c;
                            placed = true;
                            break;
                        }
                    }
                    if (!placed) {
                        cols.push([ev]);
                        ev.colIndex = cols.length - 1;
                    }
                });
                // Записываем общее количество колонок для группы
                group.forEach(ev => {
                    ev.totalCols = cols.length;
                });
            });
        });

        // --- Отрисовка блоков событий ---
        events.forEach((ev, index) => {
            const block = document.createElement('div');
            block.style.position = 'absolute';
            
            // Позиционирование
            const dateObj = new Date(ev.date + 'T00:00:00');
            const startDateObj = new Date(startDateStr + 'T00:00:00');
            const dayIndex = Math.round((dateObj - startDateObj) / (1000 * 60 * 60 * 24));
            
            const top = 40 + ev.startMin * 0.5;
            const height = (ev.endMin - ev.startMin) * 0.5;
            
            block.style.top = top + 'px';
            block.style.height = height + 'px';
            
            // Горизонтальное позиционирование (колонки)
            const dayWidth = 240;
            const colWidth = dayWidth / (ev.totalCols || 1);
            const left = 60 + dayIndex * dayWidth + (ev.colIndex || 0) * colWidth;
            
            block.style.left = left + 'px';
            block.style.width = colWidth + 'px';
            
            // Стили
            block.style.backgroundColor = PALETTE[index % 8];
            block.style.border = '1px solid #9aa0a6';
            block.style.boxSizing = 'border-box';
            block.style.padding = '1px 4px';
            block.style.overflow = 'hidden';
            block.style.color = '#3c4043';
            block.style.fontSize = '11px';
            block.style.lineHeight = '1';
            
            // Безопасная вставка текста (экранирование HTML)
            block.textContent = ev.title;
            
            container.appendChild(block);
        });
    }

    // Запуск после загрузки DOM
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();