// State
let tasks = [];
let schedule = [];
let currentPage = 'dashboard';
// Day names in Russian
const dayNames = {
mon: 'Понедельник',
tue: 'Вторник',
wed: 'Среда',
thu: 'Четверг',
fri: 'Пятница',
sat: 'Суббота',
sun: 'Воскресенье'
};
const dayNamesShort = {
mon: 'Пн',
tue: 'Вт',
wed: 'Ср',
thu: 'Чт',
fri: 'Пт',
sat: 'Сб',
sun: 'Вс'
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadDate();
loadTasks();
loadSchedule();
setupNavigation();
checkReminders();
});
// Load current date
function loadDate() {
const now = new Date();
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
document.getElementById('current-date').textContent = now.toLocaleDateString('ru-RU', options);
}
// Navigation
function setupNavigation() {
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const page = link.dataset.page;
navigateTo(page);
});
});
}
function navigateTo(page) {
currentPage = page;
document.querySelectorAll('.page').forEach(p => p.style.display = 'none');
document.getElementById('page-' + page).style.display = 'block';
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
document.querySelector(`[data-page="${page}"]`).classList.add('active');
const titles = {
dashboard: 'Главная',
tasks: 'Задания',
schedule: 'Расписание',
calendar: 'Календарь',
progress: 'Прогресс'
};
document.getElementById('page-title').textContent = titles[page];
if (page === 'tasks') renderTasks();
if (page === 'schedule') renderScheduleFull();
if (page === 'calendar') renderCalendar();
if (page === 'progress') renderProgress();
}
// Load tasks from API
async function loadTasks() {
try {
const res = await fetch('/api/tasks');
tasks = await res.json();
updateStats();
renderDashboardTasks();
} catch (e) {
console.error('Failed to load tasks:', e);
}
}
// Load schedule from API
async function loadSchedule() {
try {
const res = await fetch('/api/schedule');
schedule = await res.json();
renderDashboardSchedule();
} catch (e) {
console.error('Failed to load schedule:', e);
}
}
// Update statistics
function updateStats() {
const today = new Date().toISOString().slice(0, 10);
const stats = {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
pending: tasks.filter(t => t.status === 'pending').length,
overdue: tasks.filter(t => t.status === 'overdue' || (t.status === 'pending' && t.dueDate < today)).length
};
document.getElementById('stat-total').textContent = stats.total;
document.getElementById('stat-completed').textContent = stats.completed;
document.getElementById('stat-pending').textContent = stats.pending;
document.getElementById('stat-overdue').textContent = stats.overdue;
}
// Render dashboard tasks
function renderDashboardTasks() {
const today = new Date().toISOString().slice(0, 10);
const upcoming = tasks
.filter(t => t.status !== 'completed' && t.dueDate >= today)
.sort((a, b) => a.dueDate.localeCompare(b.dueDate))
.slice(0, 5);
const container = document.getElementById('dashboard-tasks');
if (upcoming.length === 0) {
container.innerHTML = '
';
return;
}
container.innerHTML = upcoming.map(t => renderTaskItem(t)).join('');
}
// Render task item
function renderTaskItem(t) {
const today = new Date().toISOString().slice(0, 10);
const isOverdue = t.status === 'pending' && t.dueDate < today;
const isCompleted = t.status === 'completed';
return `
${escapeHtml(t.title)}
${escapeHtml(t.subject)}
${getPriorityLabel(t.priority)}
📅 ${formatDate(t.dueDate)}
`;
}
// Render dashboard schedule
function renderDashboardSchedule() {
const today = new Date().toLocaleDateString('en-US', { weekday: 'lowercase' });
const dayMap = { monday: 'mon', tuesday: 'tue', wednesday: 'wed', thursday: 'thu', friday: 'fri', saturday: 'sat', sunday: 'sun' };
const todayKey = dayMap[today];
const todaySchedule = schedule.filter(s => s.day === todayKey).sort((a, b) => a.time.localeCompare(b.time));
const container = document.getElementById('dashboard-schedule');
if (todaySchedule.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = todaySchedule.map(s => `
${escapeHtml(s.subject)}
🕐 ${s.time}
${s.location ? `
📍 ${escapeHtml(s.location)}
` : ''}
`).join('');
}
// Render all tasks
function renderTasks() {
const filter = document.getElementById('task-filter').value;
const today = new Date().toISOString().slice(0, 10);
let filtered = [...tasks];
if (filter === 'completed') {
filtered = filtered.filter(t => t.status === 'completed');
} else if (filter === 'pending') {
filtered = filtered.filter(t => t.status === 'pending');
} else if (filter === 'overdue') {
filtered = filtered.filter(t => t.status === 'overdue' || (t.status === 'pending' && t.dueDate < today));
}
filtered.sort((a, b) => {
if (a.status === 'completed' && b.status !== 'completed') return 1;
if (a.status !== 'completed' && b.status === 'completed') return -1;
return a.dueDate.localeCompare(b.dueDate);
});
const container = document.getElementById('tasks-list');
if (filtered.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = filtered.map(t => renderTaskItem(t)).join('');
}
// Render full schedule
function renderScheduleFull() {
const container = document.getElementById('schedule-full');
const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
container.innerHTML = days.map(day => {
const daySchedule = schedule.filter(s => s.day === day).sort((a, b) => a.time.localeCompare(b.time));
return `
${dayNames[day]}
${daySchedule.length === 0 ? '
Нет занятий
' : ''}
${daySchedule.map(s => `
${escapeHtml(s.subject)}
🕐 ${s.time}
${s.location ? `
📍 ${escapeHtml(s.location)}
` : ''}
`).join('')}
`;
}).join('');
}
// Render calendar
function renderCalendar() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const today = new Date().toISOString().slice(0, 10);
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDay = firstDay.getDay() || 7;
const totalDays = lastDay.getDate();
const monthNames = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
let html = `${monthNames[month]} ${year}
`;
html += '';
// Day headers
['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].forEach(d => {
html += `
${d}
`;
});
// Empty cells before first day
for (let i = 1; i < startDay; i++) {
html += '
';
}
// Days
for (let day = 1; day <= totalDays; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const dayTasks = tasks.filter(t => t.dueDate === dateStr);
const isToday = dateStr === today;
const hasTasks = dayTasks.length > 0;
html += `
${day}
${dayTasks.slice(0, 2).map(t => `
${escapeHtml(t.title)}
`).join('')}
${dayTasks.length > 2 ? `
+${dayTasks.length - 2} ещё
` : ''}
`;
}
html += '
';
document.getElementById('calendar-view').innerHTML = html;
}
// Render progress
function renderProgress() {
const total = tasks.length;
const completed = tasks.filter(t => t.status === 'completed').length;
const pending = tasks.filter(t => t.status === 'pending').length;
const today = new Date().toISOString().slice(0, 10);
const overdue = tasks.filter(t => t.status === 'overdue' || (t.status === 'pending' && t.dueDate < today)).length;
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
const circumference = 2 * Math.PI * 45;
const offset = circumference - (percent / 100) * circumference;
document.getElementById('progress-ring').style.strokeDashoffset = offset;
document.getElementById('progress-percent').textContent = percent + '%';
document.getElementById('progress-total').textContent = total;
document.getElementById('progress-completed').textContent = completed;
document.getElementById('progress-pending').textContent = pending;
document.getElementById('progress-overdue').textContent = overdue;
// By subject
const bySubject = {};
tasks.forEach(t => {
bySubject[t.subject] = (bySubject[t.subject] || 0) + 1;
});
const container = document.getElementById('progress-by-subject');
const subjects = Object.entries(bySubject).sort((a, b) => b[1] - a[1]);
if (subjects.length === 0) {
container.innerHTML = 'Нет данных
';
return;
}
container.innerHTML = subjects.map(([subject, count]) => `
${escapeHtml(subject)}
${count} зад.
`).join('');
}
// Modal functions
function openTaskModal() {
document.getElementById('task-modal').classList.add('open');
document.getElementById('task-form').reset();
document.querySelector('[name="dueDate"]').value = new Date().toISOString().slice(0, 10);
}
function closeTaskModal() {
document.getElementById('task-modal').classList.remove('open');
}
function openScheduleModal() {
document.getElementById('schedule-modal').classList.add('open');
document.getElementById('schedule-form').reset();
}
function closeScheduleModal() {
document.getElementById('schedule-modal').classList.remove('open');
}
// Save task
async function saveTask(e) {
e.preventDefault();
const form = e.target;
const task = {
title: form.title.value,
subject: form.subject.value,
dueDate: form.dueDate.value,
priority: form.priority.value,
description: form.description.value,
status: 'pending'
};
try {
const res = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task)
});
if (res.ok) {
closeTaskModal();
await loadTasks();
renderDashboardTasks();
}
} catch (err) {
console.error('Failed to save task:', err);
}
}
// Toggle task completion
async function toggleTask(id) {
const task = tasks.find(t => t.id === id);
if (!task) return;
const newStatus = task.status === 'completed' ? 'pending' : 'completed';
try {
const res = await fetch(`/api/tasks/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
});
if (res.ok) {
await loadTasks();
renderDashboardTasks();
if (currentPage === 'tasks') renderTasks();
if (currentPage === 'progress') renderProgress();
}
} catch (err) {
console.error('Failed to update task:', err);
}
}
// Delete task
async function deleteTask(id) {
if (!confirm('Удалить это задание?')) return;
try {
const res = await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
if (res.ok) {
await loadTasks();
renderDashboardTasks();
if (currentPage === 'tasks') renderTasks();
if (currentPage === 'progress') renderProgress();
}
} catch (err) {
console.error('Failed to delete task:', err);
}
}
// Save schedule
async function saveSchedule(e) {
e.preventDefault();
const form = e.target;
const item = {
subject: form.subject.value,
day: form.day.value,
time: form.time.value,
location: form.location.value
};
try {
const res = await fetch('/api/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
if (res.ok) {
closeScheduleModal();
await loadSchedule();
renderDashboardSchedule();
if (currentPage === 'schedule') renderScheduleFull();
}
} catch (err) {
console.error('Failed to save schedule:', err);
}
}
// Check reminders
function checkReminders() {
const today = new Date().toISOString().slice(0, 10);
const upcoming = tasks.filter(t => {
if (t.status === 'completed') return false;
const diff = (new Date(t.dueDate) - new Date(today)) / (1000 * 60 * 60 * 24);
return diff >= 0 && diff <= 1;
});
if (upcoming.length > 0) {
// Could show notification here
console.log('Upcoming deadlines:', upcoming.length);
}
}
// Helpers
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function getPriorityLabel(p) {
return { high: 'Высокий', medium: 'Средний', low: 'Низкий' }[p];
}
function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
}
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.classList.remove('open');
}
});
});