chto-sozdat-veb-prilozhenie/app.js

494 lines
16 KiB
JavaScript
Raw Permalink 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.

// 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');
}
});
});