Files
kspsuti-teacher-schedule/templates/index.html
2026-03-02 14:21:02 +04:00

730 lines
27 KiB
HTML
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.
<!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>