Initial commit

This commit is contained in:
kilyabin
2026-03-02 14:21:02 +04:00
commit d78668ce3a
11 changed files with 1278 additions and 0 deletions

729
templates/index.html Normal file
View File

@@ -0,0 +1,729 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Расписание преподавателей КС ПГУТИ</title>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@300;400;600;700&family=Onest:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0d0f14;
--surface: #13161e;
--surface2: #1a1e28;
--border: #252a38;
--accent: #4f8ef7;
--accent-glow: rgba(79,142,247,0.15);
--text: #e8eaf0;
--text-muted: #6b7280;
--text-dim: #9ca3af;
--success: #34d399;
--r: 14px;
--font-d: 'Unbounded', sans-serif;
--font-b: 'Onest', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-b);
min-height: 100vh;
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
body::before {
content: '';
position: fixed; top: -20%; left: -10%;
width: 70vw; height: 70vw;
background: radial-gradient(circle, rgba(79,142,247,0.07) 0%, transparent 70%);
pointer-events: none; z-index: 0;
}
.app {
position: relative; z-index: 1;
max-width: 1400px; margin: 0 auto;
padding: 0 16px 80px;
}
/* ── Header ── */
header {
padding: 24px 0 20px;
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.header-left h1 {
font-family: var(--font-d);
font-size: clamp(1rem, 4vw, 1.4rem);
font-weight: 600; letter-spacing: -0.02em; line-height: 1.2;
}
.header-left h1 span { color: var(--accent); }
.header-left .subtitle {
font-size: 0.78rem; color: var(--text-muted);
margin-top: 5px; font-weight: 300;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 55vw;
}
.live-badge {
display: flex; align-items: center; gap: 6px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 100px; padding: 6px 12px;
font-size: 0.7rem; color: var(--text-dim);
white-space: nowrap; flex-shrink: 0; margin-top: 2px;
}
.live-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--success); box-shadow: 0 0 6px var(--success);
animation: pulse 2s infinite; flex-shrink: 0;
}
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
/* ── Teacher search ── */
.teachers-section { margin-bottom: 20px; }
.teachers-label {
font-size: 0.65rem; font-weight: 600; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--text-muted);
margin-bottom: 8px; font-family: var(--font-d);
}
/* Поле поиска */
.search-wrap { position: relative; margin-bottom: 8px; }
.search-input {
width: 100%; background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 10px 14px 10px 38px;
font-family: var(--font-b); font-size: 0.85rem; color: var(--text);
outline: none; transition: border-color .2s;
-webkit-appearance: none;
}
.search-input::placeholder { color: var(--text-muted); }
.search-input:focus { border-color: var(--accent); }
.search-icon {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--text-muted); pointer-events: none;
width: 16px; height: 16px;
}
.search-clear {
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
background: var(--surface2); border: 1px solid var(--border);
border-radius: 6px; color: var(--text-muted); cursor: pointer;
padding: 2px 6px; font-size: 0.7rem; display: none; line-height: 1.4;
transition: all .15s;
}
.search-clear:hover { color: var(--text); border-color: var(--accent); }
/* Выпадающий список */
.search-dropdown {
position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 100;
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,.4);
max-height: 280px; overflow-y: auto;
display: none;
}
.search-dropdown.open { display: block; }
.search-dropdown::-webkit-scrollbar { width: 4px; }
.search-dropdown::-webkit-scrollbar-track { background: var(--surface); }
.search-dropdown::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.dd-item {
padding: 10px 14px; font-size: 0.82rem; cursor: pointer;
transition: background .12s; border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.dd-item:last-child { border-bottom: none; }
.dd-item:hover, .dd-item.selected { background: var(--surface2); }
.dd-item.selected { color: var(--accent); }
.dd-item-name { flex: 1; min-width: 0; }
.dd-item-name mark {
background: rgba(79,142,247,.25); color: var(--accent);
border-radius: 2px; padding: 0 1px;
}
.dd-item-id {
font-family: var(--font-d); font-size: 0.6rem; color: var(--text-muted);
flex-shrink: 0;
}
.dd-empty {
padding: 20px 14px; text-align: center;
color: var(--text-muted); font-size: 0.82rem;
}
.dd-loading {
padding: 16px 14px; text-align: center;
color: var(--text-muted); font-size: 0.8rem;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
/* Таб избранных из конфига */
.fav-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
.teacher-tab {
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 7px 14px;
font-family: var(--font-b); font-size: 0.78rem; font-weight: 500;
color: var(--text-dim); cursor: pointer;
transition: all 0.18s; white-space: nowrap;
min-height: 34px; display: flex; align-items: center;
}
.teacher-tab:hover { background: var(--surface2); border-color: rgba(79,142,247,0.4); color: var(--text); }
.teacher-tab.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); font-weight: 600; }
@keyframes shimmer { 0%,100%{opacity:.4} 50%{opacity:.8} }
.teacher-tab-skeleton {
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; height: 34px; width: 120px;
animation: shimmer 1.4s infinite;
}
/* ── Week nav — ключевое изменение ── */
.week-nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 8px;
margin-bottom: 20px;
}
.week-btn {
display: flex; align-items: center; justify-content: center; gap: 6px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 10px 14px;
font-family: var(--font-b); font-size: 0.8rem; font-weight: 500;
color: var(--text-dim); cursor: pointer; transition: all 0.2s;
min-height: 42px; width: 100%;
}
.week-btn:hover:not(:disabled) { background: var(--surface2); border-color: var(--accent); color: var(--accent); }
.week-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.week-btn svg { width: 15px; height: 15px; flex-shrink: 0; }
/* Скрыть текст кнопок на узких экранах, оставить только стрелку */
.week-btn-text { display: inline; }
.week-label {
font-family: var(--font-d); font-size: clamp(0.6rem, 2.5vw, 0.8rem);
font-weight: 400; color: var(--text);
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 10px 10px;
text-align: center; line-height: 1.3;
min-height: 42px; display: flex; align-items: center; justify-content: center;
white-space: nowrap;
}
@media (max-width: 480px) {
.week-btn-text { display: none; }
.week-btn { padding: 10px; }
}
/* ── Stats — горизонтальная полоса ── */
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 20px;
}
@media (max-width: 600px) {
.stats-bar { grid-template-columns: repeat(2, 1fr); }
}
.stat {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 12px;
display: flex; align-items: center; gap: 10px;
}
.stat-icon {
width: 30px; height: 30px; border-radius: 8px;
background: var(--accent-glow);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.stat-icon svg { width: 14px; height: 14px; color: var(--accent); }
.stat-num { font-family: var(--font-d); font-size: 1.1rem; font-weight: 700; line-height: 1; }
.stat-label { font-size: 0.65rem; color: var(--text-muted); margin-top: 2px; }
@media (max-width: 400px) {
.stat { flex-direction: column; align-items: flex-start; gap: 6px; padding: 10px; }
.stat-icon { width: 26px; height: 26px; }
.stat-num { font-size: 1rem; }
}
/* ── Loading ── */
.loading {
display: flex; flex-direction: column; align-items: center;
justify-content: center; padding: 80px 20px; gap: 16px;
}
.spinner {
width: 36px; height: 36px;
border: 2px solid var(--border); border-top-color: var(--accent);
border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading p { color: var(--text-muted); font-size: 0.85rem; }
.error-box {
background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3);
border-radius: var(--r); padding: 20px; text-align: center; color: #f87171;
}
/* ── Days grid ── */
.days-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
@media (max-width: 700px) { .days-grid { grid-template-columns: 1fr; gap: 10px; } }
/* ── Day card ── */
.day-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--r); overflow: hidden;
transition: border-color .2s;
animation: fadeUp .35s ease both;
}
.day-card.has-classes { border-color: rgba(79,142,247,.2); }
.day-card.today {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 4px 20px rgba(79,142,247,.12);
}
/* hover только на не-тач устройствах */
@media (hover: hover) {
.day-card:hover { border-color: rgba(79,142,247,.35); transform: translateY(-1px); }
}
@keyframes fadeUp { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
.day-header {
padding: 12px 14px;
display: flex; align-items: center; justify-content: space-between;
border-bottom: 1px solid var(--border); background: var(--surface2);
gap: 8px;
}
.day-name {
font-family: var(--font-d); font-size: 0.72rem; font-weight: 600;
letter-spacing: .04em; text-transform: uppercase; line-height: 1;
}
.today .day-name { color: var(--accent); }
.day-date { font-size: 0.68rem; color: var(--text-muted); margin-top: 3px; }
.today-tag {
background: var(--accent); color: #fff;
font-size: 0.58rem; font-weight: 600; font-family: var(--font-d);
letter-spacing: .04em; padding: 3px 7px; border-radius: 5px;
text-transform: uppercase; flex-shrink: 0;
}
/* ── Lesson row ── */
.lesson {
display: grid;
/* номер | время | инфо | аудитория */
grid-template-columns: 28px 1fr auto;
align-items: start;
border-bottom: 1px solid var(--border);
transition: background .15s;
padding: 10px 14px;
gap: 10px;
}
.lesson:last-child { border-bottom: none; }
@media (hover: hover) { .lesson:hover { background: var(--surface2); } }
.lesson.active-lesson { background: rgba(79,142,247,.07); }
.lesson-num {
font-family: var(--font-d); font-size: 0.65rem; font-weight: 600;
color: var(--text-muted); padding-top: 2px;
text-align: center;
}
.active-lesson .lesson-num { color: var(--accent); }
.lesson-info { min-width: 0; }
.lesson-time {
font-size: 0.68rem; color: var(--text-muted);
margin-bottom: 4px; white-space: nowrap;
}
.lesson-subject {
font-size: 0.78rem; font-weight: 600; line-height: 1.35;
margin-bottom: 5px; word-break: break-word;
}
.lesson-group { display: flex; flex-wrap: wrap; gap: 4px; }
.group-badge {
background: rgba(79,142,247,.12); border: 1px solid rgba(79,142,247,.25);
color: var(--accent); border-radius: 5px; padding: 2px 7px;
font-size: 0.65rem; font-weight: 600; font-family: var(--font-d);
}
.type-badge {
background: rgba(124,58,237,.1); border: 1px solid rgba(124,58,237,.2);
color: #a78bfa; border-radius: 5px; padding: 2px 7px;
font-size: 0.63rem; font-weight: 500;
}
.lesson-location {
display: flex; align-items: center; gap: 3px;
font-size: 0.66rem; color: var(--accent); margin-top: 5px;
}
.lesson-location svg { width: 9px; height: 9px; flex-shrink: 0; }
.lesson-room {
display: flex; align-items: flex-start; justify-content: flex-end;
padding-top: 2px; flex-shrink: 0;
}
.room-badge {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 7px; padding: 3px 7px;
font-family: var(--font-d); font-size: 0.63rem; font-weight: 600; color: var(--accent);
white-space: nowrap;
}
.empty-day {
padding: 24px 16px; text-align: center;
color: var(--text-muted); font-size: 0.78rem;
}
.empty-day-icon { font-size: 1.4rem; margin-bottom: 6px; opacity: .4; }
</style>
</head>
<body>
<div class="app">
<header>
<div class="header-left">
<h1>Расписание <span>преподавателя</span></h1>
<p class="subtitle" id="teacher-full-name">Загрузка...</p>
</div>
<div class="live-badge">
<span class="live-dot"></span>
<span id="last-updated">...</span>
</div>
</header>
<div class="teachers-section" id="teachers-section">
<div class="teachers-label">Преподаватель</div>
<!-- Поиск по всем преподавателям -->
<div class="search-wrap" id="search-wrap">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input class="search-input" id="search-input" type="text"
placeholder="Поиск по ФИО..." autocomplete="off" spellcheck="false">
<button class="search-clear" id="search-clear" onclick="clearSearch()"></button>
<div class="search-dropdown" id="search-dropdown">
<div class="dd-loading">
<div class="spinner" style="width:16px;height:16px;border-width:2px"></div>
Загружаем список...
</div>
</div>
</div>
<!-- Избранные из config.yaml (если больше одного) -->
<div class="fav-tabs" id="teachers-tabs" style="margin-top:10px">
<div class="teacher-tab-skeleton"></div>
<div class="teacher-tab-skeleton" style="width:90px"></div>
</div>
</div>
<div class="week-nav">
<button class="week-btn" id="prev-btn" disabled onclick="navigate('prev')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15,18 9,12 15,6"/></svg>
<span class="week-btn-text">Предыдущая</span>
</button>
<div class="week-label" id="week-label"></div>
<button class="week-btn" id="next-btn" disabled onclick="navigate('next')">
<span class="week-btn-text">Следующая</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9,18 15,12 9,6"/></svg>
</button>
</div>
<div class="stats-bar" id="stats-bar" style="display:none">
<div class="stat">
<div class="stat-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<div><div class="stat-num" id="stat-days">0</div><div class="stat-label">дней</div></div>
</div>
<div class="stat">
<div class="stat-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12,6 12,12 16,14"/>
</svg>
</div>
<div><div class="stat-num" id="stat-lessons">0</div><div class="stat-label">пар</div></div>
</div>
<div class="stat">
<div class="stat-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<div><div class="stat-num" id="stat-groups"></div><div class="stat-label">групп</div></div>
</div>
<div class="stat">
<div class="stat-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
</div>
<div><div class="stat-num" id="stat-hours"></div><div class="stat-label">часов</div></div>
</div>
</div>
<div id="schedule-container">
<div class="loading"><div class="spinner"></div><p>Загружаем расписание...</p></div>
</div>
</div>
<script>
let currentObj = null;
let prevWk = null, nextWk = null;
const todayStr = getTodayStr();
function getTodayStr() {
const d = new Date();
return [String(d.getDate()).padStart(2,'0'), String(d.getMonth()+1).padStart(2,'0'), d.getFullYear()].join('.');
}
function nowMin() { const d = new Date(); return d.getHours()*60+d.getMinutes(); }
function isActive(t) {
const m = t.match(/(\d{2}):(\d{2})\s*[-]\s*(\d{2}):(\d{2})/);
if (!m) return false;
const n = nowMin();
return n >= +m[1]*60 + +m[2] && n <= +m[3]*60 + +m[4];
}
let allTeachers = []; // полный список с сайта
async function loadTeachers() {
try {
// Грузим избранных из конфига
const r = await fetch('/api/teachers');
const data = await r.json();
renderFavTabs(data.teachers, data.default);
currentObj = String(data.default);
loadSchedule(currentObj, null);
} catch(e) {
document.getElementById('teachers-tabs').innerHTML =
`<span style="color:var(--text-muted);font-size:.8rem">Ошибка: ${e.message}</span>`;
}
// Фоном грузим полный список для поиска
loadAllTeachers();
}
async function loadAllTeachers() {
try {
const r = await fetch('/api/all-teachers');
const data = await r.json();
allTeachers = data.teachers;
} catch(e) {
allTeachers = [];
}
}
function renderFavTabs(teachers, defaultId) {
const tabs = document.getElementById('teachers-tabs');
if (teachers.length <= 1) {
tabs.style.display = 'none';
return;
}
tabs.innerHTML = teachers.map(t => `
<button class="teacher-tab${String(t.id)===String(defaultId)?' active':''}"
data-id="${t.id}" title="${t.name}"
onclick="switchTeacher('${t.id}',this)">${t.short}</button>
`).join('');
}
function switchTeacher(id, btn) {
if (String(id) === currentObj) return;
// Снять активный у табов
document.querySelectorAll('.teacher-tab').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
currentObj = String(id);
prevWk = null; nextWk = null;
loadSchedule(id, null);
}
// ── Поиск ────────────────────────────────────────────────────────────────
const searchInput = document.getElementById('search-input');
const searchClear = document.getElementById('search-clear');
const dropdown = document.getElementById('search-dropdown');
let ddSelected = -1;
searchInput.addEventListener('input', () => {
const q = searchInput.value.trim();
searchClear.style.display = q ? 'block' : 'none';
if (!q) { closeDropdown(); return; }
renderDropdown(q);
});
searchInput.addEventListener('focus', () => {
if (searchInput.value.trim()) renderDropdown(searchInput.value.trim());
});
searchInput.addEventListener('keydown', e => {
const items = dropdown.querySelectorAll('.dd-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
ddSelected = Math.min(ddSelected + 1, items.length - 1);
highlightDD(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
ddSelected = Math.max(ddSelected - 1, 0);
highlightDD(items);
} else if (e.key === 'Enter' && ddSelected >= 0 && items[ddSelected]) {
items[ddSelected].click();
} else if (e.key === 'Escape') {
closeDropdown();
searchInput.blur();
}
});
document.addEventListener('click', e => {
if (!document.getElementById('search-wrap').contains(e.target)) closeDropdown();
});
function highlightDD(items) {
items.forEach((el, i) => el.classList.toggle('selected', i === ddSelected));
if (items[ddSelected]) items[ddSelected].scrollIntoView({ block: 'nearest' });
}
function renderDropdown(q) {
if (!allTeachers.length) {
dropdown.innerHTML = '<div class="dd-loading"><div class="spinner" style="width:14px;height:14px;border-width:2px"></div>Загружаем список...</div>';
dropdown.classList.add('open');
// Попробуем ещё раз загрузить
loadAllTeachers().then(() => { if (searchInput.value.trim()) renderDropdown(searchInput.value.trim()); });
return;
}
const norm = q.toLowerCase();
const filtered = allTeachers.filter(t => t.name.toLowerCase().includes(norm)).slice(0, 30);
ddSelected = -1;
if (!filtered.length) {
dropdown.innerHTML = `<div class="dd-empty">Не найдено: «${q}»</div>`;
} else {
dropdown.innerHTML = filtered.map(t => {
const hi = t.name.replace(new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>');
return `<div class="dd-item${String(t.id)===currentObj?' selected':''}" onclick="selectTeacher('${t.id}','${t.name.replace(/'/g,"\'")}')">
<span class="dd-item-name">${hi}</span>
</div>`;
}).join('');
}
dropdown.classList.add('open');
}
function selectTeacher(id, name) {
closeDropdown();
searchInput.value = name;
searchClear.style.display = 'block';
// Снять активный у табов
document.querySelectorAll('.teacher-tab').forEach(b => b.classList.remove('active'));
switchTeacher(id, null);
}
function clearSearch() {
searchInput.value = '';
searchClear.style.display = 'none';
closeDropdown();
searchInput.focus();
}
function closeDropdown() {
dropdown.classList.remove('open');
ddSelected = -1;
}
async function loadSchedule(obj, wk) {
document.getElementById('schedule-container').innerHTML =
`<div class="loading"><div class="spinner"></div><p>Загружаем расписание...</p></div>`;
document.getElementById('stats-bar').style.display = 'none';
const p = new URLSearchParams({ obj });
if (wk) p.set('wk', wk);
try {
const r = await fetch('/api/schedule?' + p);
render(await r.json());
} catch(e) {
document.getElementById('schedule-container').innerHTML =
`<div class="error-box">⚠️ ${e.message}</div>`;
}
}
function render(data) {
if (data.error && !data.days?.length) {
document.getElementById('schedule-container').innerHTML = `<div class="error-box">⚠️ ${data.error}</div>`;
return;
}
prevWk = data.prev_wk; nextWk = data.next_wk;
document.getElementById('prev-btn').disabled = !prevWk;
document.getElementById('next-btn').disabled = !nextWk;
document.getElementById('week-label').textContent = data.week_range || '—';
if (data.teacher_name) document.getElementById('teacher-full-name').textContent = data.teacher_name;
const activeDays = data.days.filter(d => d.has_classes).length;
const totalLessons = data.days.reduce((a,d) => a + d.lessons.filter(l=>l.has_class).length, 0);
const groups = new Set(data.days.flatMap(d=>d.lessons).filter(l=>l.group_short).map(l=>l.group_short));
document.getElementById('stat-days').textContent = activeDays;
document.getElementById('stat-lessons').textContent = totalLessons;
document.getElementById('stat-groups').textContent = groups.size || '—';
// 1 пара = 1.5 акад. часа (90 мин), но по условию 1 пара = 2 часа
document.getElementById('stat-hours').textContent = totalLessons * 2;
document.getElementById('stats-bar').style.display = 'grid';
const now = new Date();
document.getElementById('last-updated').textContent =
`${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}`;
if (!data.days.length) {
document.getElementById('schedule-container').innerHTML =
`<div class="error-box" style="background:rgba(79,142,247,.07);border-color:var(--border);color:var(--text-muted)">Расписание не найдено</div>`;
return;
}
const pin = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="9" height="9"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>`;
let html = '<div class="days-grid">';
data.days.forEach((day, i) => {
const isToday = day.date === todayStr;
const cls = ['day-card', day.has_classes?'has-classes':'', isToday?'today':''].filter(Boolean).join(' ');
html += `<div class="${cls}" style="animation-delay:${i*.04}s">
<div class="day-header">
<div>
<div class="day-name">${day.name}</div>
<div class="day-date">${day.date}</div>
</div>
${isToday ? '<span class="today-tag">Сегодня</span>' : ''}
</div>`;
if (!day.has_classes) {
html += `<div class="empty-day"><div class="empty-day-icon">☕</div>Выходной / нет пар</div>`;
} else {
day.lessons.forEach(l => {
if (!l.has_class) return;
const active = isToday && isActive(l.time);
html += `<div class="lesson${active?' active-lesson':''}">
<div class="lesson-num">${l.num}</div>
<div class="lesson-info">
<div class="lesson-time">${l.time}</div>
<div class="lesson-subject">${l.subject}</div>
<div class="lesson-group">
${l.group_short ? `<span class="group-badge">${l.group_short}</span>` : ''}
${l.lesson_type ? `<span class="type-badge">${l.lesson_type}</span>` : ''}
</div>
${l.location ? `<div class="lesson-location">${pin}${l.location}</div>` : ''}
</div>
<div class="lesson-room">${l.room ? `<div class="room-badge">${l.room}</div>` : ''}</div>
</div>`;
});
}
html += '</div>';
});
html += '</div>';
document.getElementById('schedule-container').innerHTML = html;
}
function navigate(dir) {
const wk = dir === 'prev' ? prevWk : nextWk;
if (wk) loadSchedule(currentObj, wk);
}
loadTeachers();
</script>
</body>
</html>