494 lines
16 KiB
JavaScript
494 lines
16 KiB
JavaScript
// 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 = '<div class="empty-state"><div class="empty-state-icon">🎉</div><div class="empty-state-text">Нет предстоящих заданий</div></div>';
|
||
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 `
|
||
<li class="task-item ${isCompleted ? 'completed' : ''}">
|
||
<div class="task-checkbox ${isCompleted ? 'checked' : ''}" onclick="toggleTask(${t.id})"></div>
|
||
<div class="task-content">
|
||
<div class="task-title">${escapeHtml(t.title)}</div>
|
||
<div class="task-meta">
|
||
<span class="task-subject">${escapeHtml(t.subject)}</span>
|
||
<span class="task-priority priority-${t.priority}">${getPriorityLabel(t.priority)}</span>
|
||
<span class="task-due ${isOverdue ? 'overdue' : ''}">📅 ${formatDate(t.dueDate)}</span>
|
||
</div>
|
||
</div>
|
||
<div class="task-actions">
|
||
<button class="task-action-btn" onclick="deleteTask(${t.id})">🗑️</button>
|
||
</div>
|
||
</li>
|
||
`;
|
||
}
|
||
|
||
// 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 = '<div class="empty-state"><div class="empty-state-icon">🌟</div><div class="empty-state-text">Сегодня занятий нет</div></div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = todaySchedule.map(s => `
|
||
<div class="schedule-item">
|
||
<div class="schedule-subject">${escapeHtml(s.subject)}</div>
|
||
<div class="schedule-time">🕐 ${s.time}</div>
|
||
${s.location ? `<div class="schedule-location">📍 ${escapeHtml(s.location)}</div>` : ''}
|
||
</div>
|
||
`).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 = '<div class="empty-state"><div class="empty-state-icon">📝</div><div class="empty-state-text">Нет заданий</div></div>';
|
||
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 `
|
||
<div class="schedule-day">
|
||
<div class="schedule-day-title">${dayNames[day]}</div>
|
||
${daySchedule.length === 0 ? '<div style="color: var(--text-secondary); font-size: 14px;">Нет занятий</div>' : ''}
|
||
${daySchedule.map(s => `
|
||
<div class="schedule-item">
|
||
<div class="schedule-subject">${escapeHtml(s.subject)}</div>
|
||
<div class="schedule-time">🕐 ${s.time}</div>
|
||
${s.location ? `<div class="schedule-location">📍 ${escapeHtml(s.location)}</div>` : ''}
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}).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 = `<h3 style="margin-bottom: 20px;">${monthNames[month]} ${year}</h3>`;
|
||
html += '<div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;">';
|
||
|
||
// Day headers
|
||
['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].forEach(d => {
|
||
html += `<div style="text-align: center; font-weight: 600; padding: 8px; color: var(--text-secondary);">${d}</div>`;
|
||
});
|
||
|
||
// Empty cells before first day
|
||
for (let i = 1; i < startDay; i++) {
|
||
html += '<div></div>';
|
||
}
|
||
|
||
// 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 += `
|
||
<div style="
|
||
min-height: 80px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 8px;
|
||
${isToday ? 'border-color: var(--primary); background: #EEF2FF;' : ''}
|
||
">
|
||
<div style="font-weight: 500; margin-bottom: 4px;">${day}</div>
|
||
${dayTasks.slice(0, 2).map(t => `
|
||
<div style="
|
||
font-size: 11px;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
margin-bottom: 2px;
|
||
background: ${t.priority === 'high' ? '#FEE2E2' : t.priority === 'medium' ? '#FEF3C7' : '#D1FAE5'};
|
||
color: ${t.priority === 'high' ? '#991B1B' : t.priority === 'medium' ? '#92400E' : '#065F46'};
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
">${escapeHtml(t.title)}</div>
|
||
`).join('')}
|
||
${dayTasks.length > 2 ? `<div style="font-size: 11px; color: var(--text-secondary);">+${dayTasks.length - 2} ещё</div>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
html += '</div>';
|
||
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 = '<div class="empty-state">Нет данных</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = subjects.map(([subject, count]) => `
|
||
<div class="progress-item">
|
||
<span>${escapeHtml(subject)}</span>
|
||
<strong>${count} зад.</strong>
|
||
</div>
|
||
`).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');
|
||
}
|
||
});
|
||
});
|