scheduleSon/frontend/admin/static/script.js

642 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

let currentWeekStart = null;
const weekdays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
const weekdaysShort = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
function getTodayInLondon() {
// Получаем текущую дату в таймзоне London
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Europe/London',
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);
}
}
});
});