scheduleSon/frontend/admin/static/script.js

643 lines
29 KiB
JavaScript
Raw Normal View History

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 = `
<div class="day-header">${formatDateFull(dateStr)}</div>
<div class="day-items" data-date="${dateStr}">
${renderDayItems(dayItems)}
</div>
`;
grid.appendChild(column);
});
}
function renderDayItems(items) {
if (items.length === 0) {
return '<div style="color: #999; font-style: italic; padding: 10px;">Нет записей</div>';
}
const tasks = items.filter(item => item.kind === 'task');
const events = items.filter(item => item.kind === 'event');
let html = '';
// Tasks
tasks.forEach(task => {
html += `
<div class="item task" data-id="${task.id}" data-kind="task">
<div class="item-title">${task.title}</div>
${task.repeat_weekly ? '<div style="font-size: 11px; color: #FF9800;">Повторяется</div>' : ''}
<div class="item-actions">
<button class="btn-edit" onclick="editItem(${task.id}, 'task')">Изменить</button>
<button class="btn-delete" onclick="deleteItem(${task.id}, 'task')">Удалить</button>
</div>
</div>
`;
});
// Events
events.sort((a, b) => a.start_time.localeCompare(b.start_time)).forEach(event => {
const endTime = calculateEndTime(event.start_time, event.duration_min);
html += `
<div class="item event" data-id="${event.id}" data-kind="event">
<div class="item-time">${event.start_time}-${endTime}</div>
<div class="item-title">${event.title}</div>
<div class="item-actions">
<button class="btn-edit" onclick="editItem(${event.id}, 'event')">Изменить</button>
<button class="btn-delete" onclick="deleteItem(${event.id}, 'event')">Удалить</button>
</div>
</div>
`;
});
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 = `
<h2>Добавить задачу</h2>
<form id="add-task-form">
<div class="form-group">
<label>Дата:</label>
<input type="date" id="task-date" value="${selectedDate || today}" min="${minDate}" max="${maxDateStr}" required>
</div>
<div class="form-group">
<label>Название:</label>
<textarea id="task-title" required></textarea>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="task-repeat-weekly">
<label for="task-repeat-weekly">Повторять каждую неделю</label>
</div>
</div>
<div class="form-group" id="copy-weekdays-group" style="display: none;">
<label>Добавить на другие дни недели:</label>
<div class="weekdays-select">
${remainingWeekdays.map(day => {
// day в формате 0=пн, 6=вс, но weekdaysShort использует JS формат (0=вс)
const jsDay = day === 6 ? 0 : day + 1;
return `
<label>
<input type="checkbox" name="copy-weekday" value="${day}">
${weekdaysShort[jsDay]}
</label>
`;
}).join('')}
</div>
</div>
<div style="margin-top: 20px;">
<button type="submit" class="btn btn-primary">Добавить</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
</form>
`;
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 = `
<h2>Добавить занятие</h2>
<form id="add-event-form">
<div class="form-group">
<label>Дата:</label>
<input type="date" id="event-date" value="${selectedDate || today}" min="${minDate}" max="${maxDateStr}" required>
</div>
<div class="form-group">
<label>Время начала:</label>
<div style="display: flex; gap: 10px;">
<select id="event-hour" required>
${Array.from({length: 13}, (_, i) => i + 8).map(h =>
`<option value="${String(h).padStart(2, '0')}">${String(h).padStart(2, '0')}</option>`
).join('')}
</select>
<select id="event-minute" required>
<option value="00">00</option>
<option value="15">15</option>
<option value="30">30</option>
<option value="45">45</option>
</select>
</div>
</div>
<div class="form-group">
<label>Длительность:</label>
<div style="display: flex; gap: 10px;">
<select id="event-duration-hour">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<select id="event-duration-minute">
<option value="00">00</option>
<option value="15">15</option>
<option value="30">30</option>
<option value="45">45</option>
</select>
</div>
</div>
<div class="form-group">
<label>Название:</label>
<textarea id="event-title" required></textarea>
</div>
<div style="margin-top: 20px;">
<button type="submit" class="btn btn-primary">Добавить</button>
<button type="button" class="btn" onclick="closeModal()">Отмена</button>
</div>
</form>
`;
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);
}
}
});
});