Загрузка данных
(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();
}
})();