let currentWeekStart = null; const weekdays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье']; const weekdaysShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; function getTodayInLondon() { // Получаем текущую дату в таймзоне London const now = new Date(); const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Moscow', year: 'numeric', month: '2-digit', day: '2-digit' }); return formatter.format(now); } function getWeekStart(dateStr = null) { if (!dateStr) { dateStr = getTodayInLondon(); } // Создаем дату из строки YYYY-MM-DD в локальном времени const [year, month, day] = dateStr.split('-').map(Number); const date = new Date(year, month - 1, day); // getDay() возвращает 0=воскресенье, 1=понедельник, ..., 6=суббота // Находим понедельник текущей недели const dayOfWeek = date.getDay(); // Если воскресенье (0), идем на 6 дней назад, иначе на (dayOfWeek-1) дней назад const diff = dayOfWeek === 0 ? -6 : -(dayOfWeek - 1); const monday = new Date(date); monday.setDate(date.getDate() + diff); // Проверяем, что получился понедельник if (monday.getDay() !== 1) { console.error('Error: getWeekStart did not return Monday!', monday.getDay()); } // Возвращаем в формате YYYY-MM-DD const yearStr = monday.getFullYear(); const monthStr = String(monday.getMonth() + 1).padStart(2, '0'); const dayStr = String(monday.getDate()).padStart(2, '0'); return `${yearStr}-${monthStr}-${dayStr}`; } function formatDate(dateStr) { // Создаем дату из строки YYYY-MM-DD в локальном времени const [year, month, day] = dateStr.split('-').map(Number); const date = new Date(year, month - 1, day); // getDay() возвращает 0=воскресенье, 1=понедельник, ..., 6=суббота // Конвертируем в формат где 0=понедельник, 6=воскресенье const jsDay = date.getDay(); const weekdayIndex = jsDay === 0 ? 6 : jsDay - 1; // 0=пн, 6=вс return `${weekdaysShort[weekdayIndex]}, ${date.getDate()}`; } function formatDateFull(dateStr) { // Создаем дату из строки YYYY-MM-DD в локальном времени const [year, month, day] = dateStr.split('-').map(Number); const date = new Date(year, month - 1, day); const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря']; // getDay() возвращает 0=воскресенье, 1=понедельник, ..., 6=суббота // Конвертируем в формат где 0=понедельник, 6=воскресенье const jsDay = date.getDay(); const weekdayIndex = jsDay === 0 ? 6 : jsDay - 1; // 0=пн, 6=вс return `${weekdays[weekdayIndex]}, ${date.getDate()} ${months[date.getMonth()]}`; } function getWeekDates(startDate) { // startDate должен быть понедельником (в формате YYYY-MM-DD) const dates = []; // Создаем дату из строки в локальном времени const [year, month, day] = startDate.split('-').map(Number); const start = new Date(year, month - 1, day); // Убеждаемся, что это понедельник const dayOfWeek = start.getDay(); if (dayOfWeek !== 1) { // Если не понедельник, находим понедельник const diff = dayOfWeek === 0 ? -6 : -(dayOfWeek - 1); start.setDate(start.getDate() + diff); } // Возвращаем 7 дней: понедельник - воскресенье const baseDate = new Date(start); for (let i = 0; i < 7; i++) { const date = new Date(baseDate); date.setDate(baseDate.getDate() + i); // Форматируем в YYYY-MM-DD const yearStr = date.getFullYear(); const monthStr = String(date.getMonth() + 1).padStart(2, '0'); const dayStr = String(date.getDate()).padStart(2, '0'); dates.push(`${yearStr}-${monthStr}-${dayStr}`); } // Проверяем, что первый день - понедельник, последний - воскресенье const [firstYear, firstMonth, firstDayNum] = dates[0].split('-').map(Number); const firstDate = new Date(firstYear, firstMonth - 1, firstDayNum); const [lastYear, lastMonth, lastDayNum] = dates[6].split('-').map(Number); const lastDate = new Date(lastYear, lastMonth - 1, lastDayNum); const firstDay = firstDate.getDay(); const lastDay = lastDate.getDay(); if (firstDay !== 1 || lastDay !== 0) { console.error('Error: Week should start on Monday and end on Sunday!', { first: dates[0], firstDay, last: dates[6], lastDay }); } return dates; } async function loadSchedule() { // Убеждаемся, что currentWeekStart - это понедельник if (!currentWeekStart) { currentWeekStart = getWeekStart(); } else { currentWeekStart = getWeekStart(currentWeekStart); } const weekDates = getWeekDates(currentWeekStart); const fromDate = weekDates[0]; const toDate = weekDates[6]; // Проверяем, что неделя начинается с понедельника и заканчивается воскресеньем const [mondayYear, mondayMonth, mondayDay] = weekDates[0].split('-').map(Number); const monday = new Date(mondayYear, mondayMonth - 1, mondayDay); const [sundayYear, sundayMonth, sundayDay] = weekDates[6].split('-').map(Number); const sunday = new Date(sundayYear, sundayMonth - 1, sundayDay); if (monday.getDay() !== 1 || sunday.getDay() !== 0) { console.error('Week should start on Monday and end on Sunday! Fixing...', { monday: weekDates[0], mondayDay: monday.getDay(), sunday: weekDates[6], sundayDay: sunday.getDay() }); // Исправляем автоматически без рекурсии currentWeekStart = getWeekStart(); const correctedWeekDates = getWeekDates(currentWeekStart); // Используем исправленные даты напрямую const correctedFromDate = correctedWeekDates[0]; const correctedToDate = correctedWeekDates[6]; try { const response = await fetch(`/api/schedule?from_date=${correctedFromDate}&to_date=${correctedToDate}`); const data = await response.json(); renderSchedule(correctedWeekDates, data.items); document.getElementById('week-range').textContent = `${formatDateFull(correctedWeekDates[0])} - ${formatDateFull(correctedWeekDates[6])}`; updateWeekNavigationButtons(); } catch (error) { console.error('Error loading schedule:', error); } return; } try { const response = await fetch(`/api/schedule?from_date=${fromDate}&to_date=${toDate}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (!data.items) { console.error('Invalid response format:', data); return; } renderSchedule(weekDates, data.items); // Обновляем заголовок недели document.getElementById('week-range').textContent = `${formatDateFull(weekDates[0])} - ${formatDateFull(weekDates[6])}`; // Обновляем кнопки навигации updateWeekNavigationButtons(); } catch (error) { console.error('Error loading schedule:', error); } } function renderSchedule(weekDates, items) { const grid = document.getElementById('schedule-grid'); if (!grid) { console.error('Schedule grid not found!'); return; } grid.innerHTML = ''; weekDates.forEach(dateStr => { const dayItems = items.filter(item => item.date === dateStr); const column = document.createElement('div'); column.className = 'day-column'; column.innerHTML = `
${formatDateFull(dateStr)}
${renderDayItems(dayItems)}
`; grid.appendChild(column); }); } function renderDayItems(items) { if (items.length === 0) { return '
Нет записей
'; } const tasks = items.filter(item => item.kind === 'task'); const events = items.filter(item => item.kind === 'event'); let html = ''; // Tasks tasks.forEach(task => { html += `
${task.title}
${task.repeat_weekly ? '
Повторяется
' : ''}
`; }); // Events events.sort((a, b) => a.start_time.localeCompare(b.start_time)).forEach(event => { const endTime = calculateEndTime(event.start_time, event.duration_min); html += `
${event.start_time}-${endTime}
${event.title}
`; }); return html; } function calculateEndTime(startTime, durationMin) { const [hour, minute] = startTime.split(':').map(Number); const totalMinutes = hour * 60 + minute + durationMin; const endHour = Math.floor(totalMinutes / 60); const endMinute = totalMinutes % 60; return `${String(endHour).padStart(2, '0')}:${String(endMinute).padStart(2, '0')}`; } function showModal(content) { document.getElementById('modal-body').innerHTML = content; document.getElementById('modal').style.display = 'block'; } function closeModal() { document.getElementById('modal').style.display = 'none'; } function showAddTaskModal(selectedDate = null) { const today = getTodayInLondon(); const todayWeekStart = getWeekStart(today); const nextWeekStart = new Date(todayWeekStart + 'T00:00:00'); nextWeekStart.setDate(nextWeekStart.getDate() + 7); const nextWeekStartStr = nextWeekStart.toISOString().split('T')[0]; // Ограничиваем выбор дат: только текущая и следующая неделя const minDate = todayWeekStart; const maxDate = new Date(nextWeekStartStr + 'T00:00:00'); maxDate.setDate(maxDate.getDate() + 6); // Воскресенье следующей недели const maxDateStr = maxDate.toISOString().split('T')[0]; const selectedDateObj = selectedDate ? new Date(selectedDate + 'T00:00:00') : new Date(today + 'T00:00:00'); // getDay() возвращает 0=воскресенье, 1=понедельник, ..., 6=суббота // Конвертируем в формат 0=понедельник, 6=воскресенье const jsWeekday = selectedDateObj.getDay(); const selectedWeekday = jsWeekday === 0 ? 6 : jsWeekday - 1; // 0=пн, 6=вс const remainingWeekdays = []; // Оставшиеся дни недели после выбранного (среда=2, четверг=3, пятница=4, суббота=5, воскресенье=6) for (let i = selectedWeekday + 1; i <= 6; i++) { remainingWeekdays.push(i); } const content = `

Добавить задачу

`; showModal(content); document.getElementById('task-repeat-weekly').addEventListener('change', function() { document.getElementById('copy-weekdays-group').style.display = this.checked ? 'none' : 'block'; }); document.getElementById('add-task-form').addEventListener('submit', async (e) => { e.preventDefault(); const date = document.getElementById('task-date').value; const title = document.getElementById('task-title').value; const repeatWeekly = document.getElementById('task-repeat-weekly').checked; const copyWeekdays = Array.from(document.querySelectorAll('input[name="copy-weekday"]:checked')) .map(cb => parseInt(cb.value)); try { const response = await fetch('/api/events?kind=task', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ date, title, repeat_weekly: repeatWeekly, copy_to_weekdays: copyWeekdays.length > 0 ? copyWeekdays : null }) }); if (response.ok) { const result = await response.json(); console.log('Task created:', result); closeModal(); // Небольшая задержка перед обновлением, чтобы БД успела сохранить setTimeout(() => { loadSchedule().catch(err => console.error('Error reloading schedule:', err)); }, 100); } else { const error = await response.json(); console.error('Error creating task:', error); alert('Ошибка: ' + (error.detail || 'Неизвестная ошибка')); } } catch (error) { console.error('Exception creating task:', error); alert('Ошибка при добавлении задачи: ' + error.message); } }); } function showAddEventModal(selectedDate = null) { const today = getTodayInLondon(); const todayWeekStart = getWeekStart(today); const nextWeekStart = new Date(todayWeekStart + 'T00:00:00'); nextWeekStart.setDate(nextWeekStart.getDate() + 7); const nextWeekStartStr = nextWeekStart.toISOString().split('T')[0]; // Ограничиваем выбор дат: только текущая и следующая неделя const minDate = todayWeekStart; const maxDate = new Date(nextWeekStartStr + 'T00:00:00'); maxDate.setDate(maxDate.getDate() + 6); // Воскресенье следующей недели const maxDateStr = maxDate.toISOString().split('T')[0]; const content = `

Добавить занятие

`; showModal(content); document.getElementById('add-event-form').addEventListener('submit', async (e) => { e.preventDefault(); const date = document.getElementById('event-date').value; const hour = document.getElementById('event-hour').value; const minute = document.getElementById('event-minute').value; const durHour = parseInt(document.getElementById('event-duration-hour').value); const durMin = parseInt(document.getElementById('event-duration-minute').value); const title = document.getElementById('event-title').value; const durationMin = durHour * 60 + durMin; if (durationMin === 0) { alert('Длительность должна быть минимум 15 минут'); return; } try { const response = await fetch('/api/events?kind=event', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ date, start_time: `${hour}:${minute}`, duration_min: durationMin, title }) }); if (response.ok) { const result = await response.json(); console.log('Event created:', result); closeModal(); // Небольшая задержка перед обновлением, чтобы БД успела сохранить setTimeout(() => { loadSchedule().catch(err => console.error('Error reloading schedule:', err)); }, 100); } else { const error = await response.json(); console.error('Error creating event:', error); alert('Ошибка: ' + (error.detail || 'Неизвестная ошибка')); } } catch (error) { console.error('Exception creating event:', error); alert('Ошибка при добавлении занятия: ' + error.message); } }); } async function editItem(id, kind) { // TODO: Реализовать редактирование alert('Редактирование будет реализовано позже'); } async function deleteItem(id, kind) { if (!confirm('Удалить эту запись?')) return; try { const response = await fetch(`/api/events/${id}`, { method: 'DELETE' }); if (response.ok) { // Небольшая задержка перед обновлением setTimeout(() => { loadSchedule().catch(err => console.error('Error reloading schedule:', err)); }, 100); } else { const error = await response.json(); alert('Ошибка при удалении: ' + (error.detail || 'Неизвестная ошибка')); } } catch (error) { alert('Ошибка при удалении: ' + error.message); console.error(error); } } function updateWeekNavigationButtons() { const today = getTodayInLondon(); const todayWeekStart = getWeekStart(today); // Вычисляем следующую неделю const [year, month, day] = todayWeekStart.split('-').map(Number); const nextWeekStartDate = new Date(year, month - 1, day); nextWeekStartDate.setDate(nextWeekStartDate.getDate() + 7); const nextWeekStartStr = `${nextWeekStartDate.getFullYear()}-${String(nextWeekStartDate.getMonth() + 1).padStart(2, '0')}-${String(nextWeekStartDate.getDate()).padStart(2, '0')}`; // Создаем даты для сравнения const [currYear, currMonth, currDay] = currentWeekStart.split('-').map(Number); const currentWeekStartDate = new Date(currYear, currMonth - 1, currDay); const [todayYear, todayMonth, todayDay] = todayWeekStart.split('-').map(Number); const todayWeekStartDate = new Date(todayYear, todayMonth - 1, todayDay); const [nextYear, nextMonth, nextDay] = nextWeekStartStr.split('-').map(Number); const nextWeekStartDateObj = new Date(nextYear, nextMonth - 1, nextDay); // Отключаем кнопку "предыдущая неделя", если уже на текущей неделе const prevButton = document.getElementById('prev-week'); if (currentWeekStartDate.getTime() <= todayWeekStartDate.getTime()) { prevButton.disabled = true; prevButton.style.opacity = '0.5'; prevButton.style.cursor = 'not-allowed'; } else { prevButton.disabled = false; prevButton.style.opacity = '1'; prevButton.style.cursor = 'pointer'; } // Отключаем кнопку "следующая неделя", если уже на следующей неделе const nextButton = document.getElementById('next-week'); if (currentWeekStartDate.getTime() >= nextWeekStartDateObj.getTime()) { nextButton.disabled = true; nextButton.style.opacity = '0.5'; nextButton.style.cursor = 'not-allowed'; } else { nextButton.disabled = false; nextButton.style.opacity = '1'; nextButton.style.cursor = 'pointer'; } } // Инициализация document.addEventListener('DOMContentLoaded', () => { currentWeekStart = getWeekStart(); loadSchedule(); updateWeekNavigationButtons(); document.getElementById('prev-week').addEventListener('click', () => { const today = getTodayInLondon(); const todayWeekStart = getWeekStart(today); // Создаем даты для сравнения const [currYear, currMonth, currDay] = currentWeekStart.split('-').map(Number); const currentWeekStartDate = new Date(currYear, currMonth - 1, currDay); const [todayYear, todayMonth, todayDay] = todayWeekStart.split('-').map(Number); const todayWeekStartDate = new Date(todayYear, todayMonth - 1, todayDay); // Разрешаем только текущую и следующую неделю // Если текущая неделя - это сегодняшняя неделя, не разрешаем идти назад if (currentWeekStartDate.getTime() <= todayWeekStartDate.getTime()) { console.log('Already on current week, cannot go back'); return; // Уже на текущей неделе, нельзя идти назад } // Переходим на предыдущую неделю const prevWeekDate = new Date(currYear, currMonth - 1, currDay); prevWeekDate.setDate(prevWeekDate.getDate() - 7); currentWeekStart = `${prevWeekDate.getFullYear()}-${String(prevWeekDate.getMonth() + 1).padStart(2, '0')}-${String(prevWeekDate.getDate()).padStart(2, '0')}`; // Убеждаемся, что это понедельник currentWeekStart = getWeekStart(currentWeekStart); console.log('Moving to previous week:', currentWeekStart); loadSchedule(); }); document.getElementById('next-week').addEventListener('click', () => { const today = getTodayInLondon(); const todayWeekStart = getWeekStart(today); // Вычисляем следующую неделю от текущей const [year, month, day] = currentWeekStart.split('-').map(Number); const nextWeekDate = new Date(year, month - 1, day); nextWeekDate.setDate(nextWeekDate.getDate() + 7); const nextWeekStartStr = `${nextWeekDate.getFullYear()}-${String(nextWeekDate.getMonth() + 1).padStart(2, '0')}-${String(nextWeekDate.getDate()).padStart(2, '0')}`; // Вычисляем максимально допустимую следующую неделю (от сегодня) const [todayYear, todayMonth, todayDay] = todayWeekStart.split('-').map(Number); const maxNextWeekDate = new Date(todayYear, todayMonth - 1, todayDay); maxNextWeekDate.setDate(maxNextWeekDate.getDate() + 7); const maxNextWeekStr = `${maxNextWeekDate.getFullYear()}-${String(maxNextWeekDate.getMonth() + 1).padStart(2, '0')}-${String(maxNextWeekDate.getDate()).padStart(2, '0')}`; // Разрешаем только текущую и следующую неделю if (nextWeekStartStr > maxNextWeekStr) { console.log('Already on next week, cannot go further'); return; // Уже на следующей неделе, нельзя идти дальше } // Переходим на следующую неделю currentWeekStart = nextWeekStartStr; // Убеждаемся, что это понедельник currentWeekStart = getWeekStart(currentWeekStart); console.log('Moving to next week:', currentWeekStart); loadSchedule(); }); document.getElementById('add-task-btn').addEventListener('click', () => showAddTaskModal()); document.getElementById('add-event-btn').addEventListener('click', () => showAddEventModal()); document.querySelector('.close').addEventListener('click', closeModal); window.addEventListener('click', (e) => { const modal = document.getElementById('modal'); if (e.target === modal) { closeModal(); } }); // Клик по дню для добавления document.addEventListener('click', (e) => { if (e.target.closest('.day-items')) { const date = e.target.closest('.day-items').dataset.date; if (e.ctrlKey || e.metaKey) { showAddEventModal(date); } else if (e.shiftKey) { showAddTaskModal(date); } } }); });