Files
magistr/frontend/teacher/index.html

532 lines
16 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>
<style>
* {
box-sizing: border-box;
}
:root {
--bg: #f5f6f8;
--panel: #ffffff;
--panel-border: #d8dee8;
--text: #172033;
--muted: #67758d;
--accent: #1d4ed8;
--accent-strong: #1e40af;
--green: #047857;
--orange: #b45309;
--shadow: 0 12px 30px rgba(23, 32, 51, 0.08);
}
[data-theme="dark"] {
--bg: #10151f;
--panel: #171f2e;
--panel-border: #2a374b;
--text: #eef4fb;
--muted: #a2b0c5;
--accent: #60a5fa;
--accent-strong: #93c5fd;
--green: #34d399;
--orange: #f59e0b;
--shadow: 0 18px 42px rgba(0, 0, 0, 0.28);
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
}
.shell {
max-width: 1240px;
margin: 0 auto;
padding: 24px;
}
.topbar,
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.topbar {
margin-bottom: 18px;
}
.title h1 {
margin: 0 0 4px;
font-size: 28px;
font-weight: 700;
letter-spacing: 0;
}
.title span {
color: var(--muted);
font-size: 14px;
}
.toolbar {
margin-bottom: 16px;
padding: 14px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
}
.actions,
.week-nav {
display: flex;
align-items: center;
gap: 8px;
}
.btn,
.date-input {
min-height: 40px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: var(--panel);
color: var(--text);
font: inherit;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
padding: 0 14px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
border-color: var(--accent);
color: var(--accent-strong);
}
.btn-primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-strong);
color: #fff;
}
.date-input {
padding: 0 12px;
}
.status {
min-height: 22px;
margin-bottom: 12px;
color: var(--muted);
font-size: 14px;
}
.schedule {
display: grid;
grid-template-columns: 96px repeat(7, minmax(0, 1fr));
border: 1px solid var(--panel-border);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.corner,
.day-head,
.time-cell,
.day-cell {
border-right: 1px solid var(--panel-border);
border-bottom: 1px solid var(--panel-border);
}
.corner,
.day-head {
min-height: 58px;
padding: 10px;
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.day-head strong {
display: block;
}
.day-head span {
color: var(--muted);
font-size: 13px;
}
.time-cell {
padding: 10px;
color: var(--muted);
font-size: 13px;
background: color-mix(in srgb, var(--panel-border) 26%, transparent);
}
.day-cell {
min-height: 118px;
padding: 8px;
}
.lesson {
height: 100%;
min-height: 96px;
border-left: 4px solid var(--green);
border-radius: 7px;
background: color-mix(in srgb, var(--green) 10%, transparent);
padding: 9px;
}
.lesson-title {
font-weight: 700;
line-height: 1.25;
}
.lesson-meta {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 8px;
}
.chip {
border: 1px solid var(--panel-border);
border-radius: 999px;
padding: 3px 8px;
color: var(--muted);
font-size: 12px;
background: var(--panel);
}
.empty {
color: var(--muted);
font-size: 13px;
}
.error {
color: var(--orange);
}
@media (max-width: 980px) {
.schedule {
display: block;
border: 0;
background: transparent;
box-shadow: none;
}
.mobile-day {
margin-bottom: 10px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: var(--panel);
overflow: hidden;
}
.mobile-day .day-head {
border-right: 0;
}
.mobile-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
}
}
@media (max-width: 640px) {
.shell {
padding: 16px;
}
.topbar,
.toolbar,
.actions {
display: grid;
align-items: stretch;
}
.week-nav .btn {
flex: 1;
}
}
</style>
</head>
<body>
<main class="shell">
<header class="topbar">
<div class="title">
<h1>Расписание преподавателя</h1>
<span id="week-label">Загрузка периода...</span>
</div>
<div class="actions">
<button class="btn" id="theme-toggle-local" type="button" aria-label="Переключить тему"></button>
<a class="btn" href="/" id="logout-link">Выйти</a>
</div>
</header>
<section class="toolbar">
<div class="week-nav">
<button class="btn" id="prev-week" type="button" aria-label="Предыдущая неделя"></button>
<button class="btn btn-primary" id="today-week" type="button">Сегодня</button>
<button class="btn" id="next-week" type="button" aria-label="Следующая неделя"></button>
</div>
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
</section>
<div class="status" id="status-line"></div>
<section class="schedule" id="schedule-grid" aria-live="polite"></section>
</main>
<script>
(() => {
const token = localStorage.getItem('token');
const teacherId = localStorage.getItem('userId');
const grid = document.getElementById('schedule-grid');
const statusLine = document.getElementById('status-line');
const weekLabel = document.getElementById('week-label');
const datePicker = document.getElementById('date-picker');
let weekStart = startOfWeek(new Date());
document.getElementById('logout-link').addEventListener('click', () => {
localStorage.removeItem('token');
localStorage.removeItem('role');
localStorage.removeItem('departmentId');
localStorage.removeItem('userId');
});
document.getElementById('theme-toggle-local').addEventListener('click', () => {
const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
});
document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light';
document.getElementById('prev-week').addEventListener('click', () => moveWeek(-1));
document.getElementById('today-week').addEventListener('click', () => {
weekStart = startOfWeek(new Date());
loadSchedule();
});
document.getElementById('next-week').addEventListener('click', () => moveWeek(1));
datePicker.addEventListener('change', () => {
if (datePicker.value) {
weekStart = startOfWeek(new Date(datePicker.value));
loadSchedule();
}
});
loadSchedule();
async function loadSchedule() {
const startDate = toIsoDate(weekStart);
const endDate = toIsoDate(addDays(weekStart, 6));
datePicker.value = startDate;
weekLabel.textContent = `${formatDate(weekStart)} - ${formatDate(addDays(weekStart, 6))}`;
if (!teacherId) {
renderEmptyWeek('Не удалось определить преподавателя');
showStatus('Выполните вход заново, чтобы получить идентификатор пользователя', true);
return;
}
renderEmptyWeek('Загрузка...');
try {
const lessons = await fetchJson(`/api/schedule?teacherId=${encodeURIComponent(teacherId)}&startDate=${startDate}&endDate=${endDate}`);
renderWeek(lessons);
showStatus(lessons.length ? `Найдено занятий: ${lessons.length}` : 'На этой неделе занятий нет', false);
} catch (error) {
renderEmptyWeek('Ошибка загрузки');
showStatus(error.message, true);
}
}
function renderWeek(lessons) {
if (window.matchMedia('(max-width: 980px)').matches) {
renderMobileWeek(lessons);
return;
}
const slots = collectSlots(lessons);
const lessonsByKey = new Map();
lessons.forEach(lesson => {
lessonsByKey.set(`${lesson.date}:${lesson.timeSlotOrder}`, lesson);
});
grid.innerHTML = [
'<div class="corner"></div>',
...daysOfWeek().map(date => renderHead(date)),
...slots.flatMap(slot => [
`<div class="time-cell">${escapeHtml(slot.label)}</div>`,
...daysOfWeek().map(date => renderCell(lessonsByKey.get(`${toIsoDate(date)}:${slot.order}`)))
])
].join('');
}
function renderMobileWeek(lessons) {
const lessonsByDate = new Map();
lessons.forEach(lesson => {
if (!lessonsByDate.has(lesson.date)) lessonsByDate.set(lesson.date, []);
lessonsByDate.get(lesson.date).push(lesson);
});
grid.innerHTML = daysOfWeek().map(date => {
const key = toIsoDate(date);
const dayLessons = lessonsByDate.get(key) || [];
return `
<article class="mobile-day">
${renderHead(date)}
<div class="mobile-list">
${dayLessons.length ? dayLessons.map(renderLesson).join('') : '<div class="empty">Занятий нет</div>'}
</div>
</article>
`;
}).join('');
}
function renderEmptyWeek(message) {
if (window.matchMedia('(max-width: 980px)').matches) {
grid.innerHTML = daysOfWeek().map(date => `
<article class="mobile-day">
${renderHead(date)}
<div class="mobile-list"><div class="empty">${escapeHtml(message)}</div></div>
</article>
`).join('');
return;
}
grid.innerHTML = [
'<div class="corner"></div>',
...daysOfWeek().map(date => renderHead(date)),
`<div class="time-cell">Неделя</div>`,
...daysOfWeek().map(() => `<div class="day-cell"><div class="empty">${escapeHtml(message)}</div></div>`)
].join('');
}
function renderHead(date) {
return `
<div class="day-head">
<strong>${escapeHtml(dayName(date))}</strong>
<span>${escapeHtml(formatDate(date))}</span>
</div>
`;
}
function renderCell(lesson) {
return `<div class="day-cell">${lesson ? renderLesson(lesson) : '<div class="empty">Нет занятия</div>'}</div>`;
}
function renderLesson(lesson) {
const groups = Array.isArray(lesson.groupNames) ? lesson.groupNames.join(', ') : '';
const subgroups = Array.isArray(lesson.subgroupNames) && lesson.subgroupNames.length
? lesson.subgroupNames.join(', ')
: lesson.subgroupName;
return `
<div class="lesson">
<div class="lesson-title">${escapeHtml(lesson.subjectName)}</div>
<div class="lesson-meta">
<span class="chip">${escapeHtml(trimTime(lesson.startTime))} - ${escapeHtml(trimTime(lesson.endTime))}</span>
<span class="chip">${escapeHtml(lesson.lessonTypeName)}</span>
${subgroups ? `<span class="chip">${escapeHtml(subgroups)}</span>` : ''}
<span class="chip">${escapeHtml(lesson.classroomName)}</span>
${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''}
</div>
</div>
`;
}
function collectSlots(lessons) {
const slots = new Map();
lessons.forEach(lesson => {
slots.set(lesson.timeSlotOrder, {
order: lesson.timeSlotOrder,
label: `${trimTime(lesson.startTime)} - ${trimTime(lesson.endTime)}`
});
});
if (!slots.size) {
slots.set(1, { order: 1, label: 'Расписание' });
}
return [...slots.values()].sort((a, b) => a.order - b.order);
}
function moveWeek(delta) {
weekStart = addDays(weekStart, delta * 7);
loadSchedule();
}
function daysOfWeek() {
return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
}
async function fetchJson(url) {
const response = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(data?.message || `Ошибка HTTP: ${response.status}`);
}
return data;
}
function showStatus(message, isError) {
statusLine.textContent = message;
statusLine.classList.toggle('error', isError);
}
function startOfWeek(date) {
const copy = new Date(date.getFullYear(), date.getMonth(), date.getDate());
const day = copy.getDay() || 7;
copy.setDate(copy.getDate() - day + 1);
return copy;
}
function addDays(date, days) {
const copy = new Date(date);
copy.setDate(copy.getDate() + days);
return copy;
}
function toIsoDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
function formatDate(date) {
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function dayName(date) {
const value = date.toLocaleDateString('ru-RU', { weekday: 'long' });
return value.charAt(0).toUpperCase() + value.slice(1);
}
function trimTime(value) {
return String(value || '').slice(0, 5);
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
})();
</script>
</body>
</html>