Files
magistr/frontend/admin/js/views/schedule-view.js

356 lines
14 KiB
JavaScript
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.
import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
const ROLE_DEPARTMENT = 'DEPARTMENT';
const PARITY_LABELS = {
BOTH: 'каждая',
ODD: 'нечётная',
EVEN: 'чётная'
};
export async function initScheduleView() {
const role = localStorage.getItem('role');
const userDepartmentId = localStorage.getItem('departmentId');
const startInput = document.getElementById('schedule-view-start');
const endInput = document.getElementById('schedule-view-end');
const loadButton = document.getElementById('schedule-view-load');
const departmentField = document.getElementById('schedule-view-department-field');
const departmentSelect = document.getElementById('schedule-view-department');
const tables = document.getElementById('schedule-view-tables');
const countLabel = document.getElementById('schedule-view-count');
setDefaultDates(startInput, endInput);
loadButton?.addEventListener('click', loadData);
try {
await loadDictionaries(role, userDepartmentId);
if (role === ROLE_DEPARTMENT) {
departmentField.hidden = true;
departmentSelect.value = userDepartmentId || '';
}
await loadData();
} catch (error) {
setTableState(error.message || 'Ошибка загрузки справочников');
}
async function loadData() {
hideAlert('schedule-view-alert');
setTableState('Загрузка...');
countLabel.textContent = 'Загрузка...';
const params = new URLSearchParams({
startDate: startInput.value,
endDate: endInput.value
});
addOptional(params, 'groupId', valueOf('schedule-view-group'));
addOptional(params, 'teacherId', valueOf('schedule-view-teacher'));
addOptional(params, 'classroomId', valueOf('schedule-view-classroom'));
addOptional(params, 'departmentId', role === ROLE_DEPARTMENT ? userDepartmentId : departmentSelect.value);
addOptional(params, 'subjectId', valueOf('schedule-view-subject'));
addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type'));
addOptional(params, 'parity', valueOf('schedule-view-parity'));
try {
const lessons = await api.get(`/api/schedule/search?${params}`);
renderLessons(lessons);
} catch (error) {
const message = error.message || 'Ошибка загрузки расписания';
showAlert('schedule-view-alert', message, 'error');
setTableState(message);
countLabel.textContent = '0 занятий';
}
}
function renderLessons(lessons) {
countLabel.textContent = lessonCountLabel(lessons.length);
if (!lessons.length) {
setTableState('Занятия не найдены');
return;
}
const slots = collectSlots(lessons);
const lessonsByDateSlot = groupLessonsByDateSlot(lessons);
const weeks = groupDatesByWeek(lessons.map(lesson => lesson.date));
tables.innerHTML = weeks
.map(week => renderWeekTable(week, slots, lessonsByDateSlot))
.join('');
}
function renderWeekTable(week, slots, lessonsByDateSlot) {
const rows = week.dates.map(date => `
<tr>
<td class="axis-cell schedule-view-date-cell">
<strong>${escapeHtml(dayTitle(date, lessonsByDateSlot))}</strong>
<span>${escapeHtml(formatDate(date))}</span>
</td>
${slots.map(slot => renderSlotCell(date, slot, lessonsByDateSlot)).join('')}
</tr>
`).join('');
return `
<section class="schedule-view-week-panel">
<div class="room-week-heading">
<h3>${escapeHtml(week.label)}</h3>
<span>${escapeHtml(weekCaption(week.dates))}</span>
</div>
<div class="workload-grid-container schedule-view-grid-container">
<table class="workload-table schedule-view-matrix-table">
<thead>
<tr>
<th class="top-left-cell">
<span class="top-label">Время</span>
<span class="bottom-label">Дата</span>
</th>
${slots.map(renderSlotHeader).join('')}
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
</section>
`;
}
function renderSlotHeader(slot) {
return `
<th>
<strong>${escapeHtml(slot.orderLabel)}</strong>
<span>${escapeHtml(slot.timeLabel)}</span>
${slot.id ? `<small>ID ${escapeHtml(slot.id)}</small>` : ''}
</th>
`;
}
function renderSlotCell(date, slot, lessonsByDateSlot) {
const lessons = lessonsByDateSlot.get(dateSlotKey(date, slot.key)) || [];
return `
<td class="${lessons.length ? 'workload-busy-cell' : 'workload-free-cell'}">
${lessons.length ? lessons.map(renderLessonCard).join('') : '<span class="free-slot">Свободно</span>'}
</td>
`;
}
function renderLessonCard(lesson) {
const subgroupText = (lesson.subgroupNames || []).join(', ');
const meta = [
lesson.lessonTypeName,
lesson.lessonFormat,
PARITY_LABELS[lesson.parity] || lesson.parity
].filter(Boolean).join(' · ');
return `
<div class="lesson-card schedule-view-lesson-card">
<div class="lesson-subject">${escapeHtml(lesson.subjectName || 'Без дисциплины')}</div>
<div class="lesson-group">${escapeHtml((lesson.groupNames || []).join(', ') || 'Группа не указана')}</div>
${subgroupText ? `<div class="lesson-subgroup">${escapeHtml(subgroupText)}</div>` : ''}
<div class="lesson-teacher">${escapeHtml(lesson.teacherName || 'Преподаватель не указан')}</div>
<div class="lesson-meta">${escapeHtml(lesson.classroomName || 'Аудитория не указана')}</div>
${meta ? `<div class="lesson-meta">${escapeHtml(meta)}</div>` : ''}
${lesson.scheduleRuleSlotId ? `<div class="lesson-meta">ID слота: ${escapeHtml(lesson.scheduleRuleSlotId)}</div>` : ''}
</div>
`;
}
function setTableState(message) {
if (!tables) return;
tables.innerHTML = `<div class="loading-row schedule-view-state">${escapeHtml(message)}</div>`;
}
}
async function loadDictionaries(role, userDepartmentId) {
const [groups, teachers, classrooms, departments, subjects, lessonTypes] = await Promise.all([
api.get('/api/groups'),
api.get('/api/users/teachers'),
api.get('/api/classrooms'),
api.get('/api/departments'),
api.get('/api/subjects'),
api.get('/api/lesson-types')
]);
fillSelect('schedule-view-group', groups, item => item.id, item => item.name, 'Все группы');
fillSelect('schedule-view-teacher', teachers, item => item.id, item => item.fullName || item.username, 'Все преподаватели');
fillSelect('schedule-view-classroom', classrooms, item => item.id, item => roomLabel(item), 'Все аудитории');
fillSelect('schedule-view-department', departments, item => item.id, item => item.departmentName, 'Все кафедры');
fillSelect('schedule-view-subject', subjects, item => item.id, item => item.name, 'Все дисциплины');
fillSelect('schedule-view-lesson-type', lessonTypes, item => item.id, item => item.name, 'Все типы');
if (role === ROLE_DEPARTMENT && userDepartmentId) {
document.getElementById('schedule-view-department').value = userDepartmentId;
}
}
function setDefaultDates(startInput, endInput) {
const today = new Date();
const nextWeek = new Date();
nextWeek.setDate(today.getDate() + 7);
startInput.value = toIsoDate(today);
endInput.value = toIsoDate(nextWeek);
}
function fillSelect(id, items, valueFn, labelFn, emptyLabel) {
const select = document.getElementById(id);
if (!select) return;
select.innerHTML = `<option value="">${escapeHtml(emptyLabel)}</option>` +
(items || []).map(item => `<option value="${escapeHtml(valueFn(item))}">${escapeHtml(labelFn(item))}</option>`).join('');
}
function addOptional(params, key, value) {
if (value) params.set(key, value);
}
function valueOf(id) {
return document.getElementById(id)?.value || '';
}
function roomLabel(room) {
return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · ');
}
function formatSlot(lesson) {
const order = lesson.timeSlotOrder ? `${lesson.timeSlotOrder} пара` : 'Пара';
const time = lesson.startTime && lesson.endTime
? ` (${trimTime(lesson.startTime)}-${trimTime(lesson.endTime)})`
: '';
return `${order}${time}`;
}
function collectSlots(lessons) {
const slots = new Map();
lessons.forEach(lesson => {
const key = lessonSlotKey(lesson);
const existing = slots.get(key);
const next = {
key,
id: lesson.timeSlotId,
orderNumber: lesson.timeSlotOrder,
startTime: lesson.startTime,
endTime: lesson.endTime,
orderLabel: lesson.timeSlotOrder ? `${lesson.timeSlotOrder} пара` : 'Пара',
timeLabel: lesson.startTime || lesson.endTime
? `${trimTime(lesson.startTime)}-${trimTime(lesson.endTime)}`
: 'Время не указано'
};
if (!existing || existing.timeLabel === 'Время не указано') {
slots.set(key, next);
}
});
return Array.from(slots.values()).sort(compareSlots);
}
function groupLessonsByDateSlot(lessons) {
const grouped = new Map();
lessons.forEach(lesson => {
if (!lesson.date) return;
const key = dateSlotKey(lesson.date, lessonSlotKey(lesson));
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(lesson);
});
grouped.forEach(items => {
items.sort((a, b) => naturalCompare(a.subjectName, b.subjectName)
|| naturalCompare(a.teacherName, b.teacherName)
|| naturalCompare((a.groupNames || []).join(','), (b.groupNames || []).join(',')));
});
return grouped;
}
function groupDatesByWeek(dates) {
const groups = new Map();
Array.from(new Set(dates.filter(Boolean)))
.sort((a, b) => naturalCompare(a, b))
.forEach(date => {
const key = weekStart(date);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(date);
});
return Array.from(groups.entries()).map(([startDate, weekDates]) => ({
startDate,
dates: weekDates,
label: `Неделя ${formatDate(startDate)} - ${formatDate(addDaysToIso(startDate, 6))}`
}));
}
function dayTitle(date, lessonsByDateSlot) {
for (const [key, lessons] of lessonsByDateSlot.entries()) {
if (key.startsWith(`${date}:`) && lessons[0]?.dayName) {
return lessons[0].dayName;
}
}
return dayName(date);
}
function weekCaption(dates) {
const first = dates[0];
const last = dates[dates.length - 1];
return `${formatDate(first)} - ${formatDate(last)}`;
}
function lessonSlotKey(lesson) {
const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`;
return value ? String(value) : 'unknown';
}
function dateSlotKey(date, slotKey) {
return `${date}:${slotKey}`;
}
function weekStart(value) {
const date = parseIsoDate(value);
const day = date.getDay() || 7;
date.setDate(date.getDate() + 1 - day);
return toIsoDate(date);
}
function addDaysToIso(value, days) {
const date = parseIsoDate(value);
date.setDate(date.getDate() + days);
return toIsoDate(date);
}
function parseIsoDate(value) {
const [year, month, day] = String(value).split('-').map(Number);
return new Date(year, month - 1, day);
}
function dayName(value) {
return parseIsoDate(value).toLocaleDateString('ru-RU', { weekday: 'long' });
}
function formatDate(value) {
if (!value) return '-';
const [year, month, day] = String(value).split('-');
return `${day}.${month}.${year}`;
}
function trimTime(value) {
return String(value || '').slice(0, 5);
}
function toIsoDate(date) {
return [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0')
].join('-');
}
function lessonCountLabel(count) {
const tail = count % 100;
if (tail >= 11 && tail <= 14) return `${count} занятий`;
if (count % 10 === 1) return `${count} занятие`;
if ([2, 3, 4].includes(count % 10)) return `${count} занятия`;
return `${count} занятий`;
}
function compareSlots(a, b) {
const first = Number(a.orderNumber || a.key);
const second = Number(b.orderNumber || b.key);
if (!Number.isNaN(first) && !Number.isNaN(second) && first !== second) {
return first - second;
}
return naturalCompare(a.startTime, b.startTime)
|| naturalCompare(a.key, b.key);
}
function naturalCompare(a, b) {
return String(a || '').localeCompare(String(b || ''), 'ru', { numeric: true, sensitivity: 'base' });
}