создал систему календарного учебного графика
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* ===== Auditorium Workload Specific Styles ===== */
|
||||
/* ===== Стили загруженности аудиторий ===== */
|
||||
|
||||
.workload-grid-container {
|
||||
width: 100%;
|
||||
@@ -37,7 +37,8 @@
|
||||
box-shadow: 0 1px 0 var(--bg-card-border);
|
||||
}
|
||||
|
||||
.workload-table .time-cell {
|
||||
.workload-table .time-cell,
|
||||
.workload-table .axis-cell {
|
||||
background: var(--bg-input);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
@@ -62,7 +63,7 @@
|
||||
box-shadow: 1px 1px 0 var(--bg-card-border);
|
||||
}
|
||||
|
||||
/* Diagonal line using SVG or linear-gradient */
|
||||
/* Диагональная линия через linear-gradient */
|
||||
.workload-table .top-left-cell::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -126,7 +127,7 @@
|
||||
}
|
||||
|
||||
.lesson-group {
|
||||
font-weight: 700; /* Bolder specific for groups request mockup */
|
||||
font-weight: 700; /* Акцент на названии группы */
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.1rem;
|
||||
font-size: 0.75rem;
|
||||
@@ -137,7 +138,7 @@
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Custom scrollbar adjustments for grid container */
|
||||
/* Настройка scrollbar для контейнера сетки */
|
||||
.workload-grid-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
||||
@@ -162,28 +162,96 @@
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.matrix-grid {
|
||||
.calendar-fill-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr)) auto;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.matrix-week {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
align-items: end;
|
||||
margin: 1rem 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.matrix-week > span {
|
||||
.academic-calendar-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.calendar-course-block {
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-course-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.calendar-course-header h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.calendar-course-header span,
|
||||
.calendar-week-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.calendar-days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
min-height: 92px;
|
||||
padding: 0.55rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.calendar-day-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.35rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.calendar-day-activity {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calendar-totals {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-total-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--chip-color) 18%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--chip-color) 52%, transparent);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.muted {
|
||||
@@ -194,12 +262,20 @@
|
||||
.schedule-slot-row {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.calendar-fill-panel {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.schedule-slot-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calendar-fill-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide Number Arrows */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { initMultiSelect } from '../utils.js';
|
||||
|
||||
export function initAuditoriumWorkload() {
|
||||
// Initialize date input with current date
|
||||
// Дата по умолчанию — текущий день.
|
||||
const dateInput = document.getElementById('workload-date');
|
||||
if (dateInput) {
|
||||
const today = new Date();
|
||||
@@ -11,20 +11,20 @@ export function initAuditoriumWorkload() {
|
||||
dateInput.value = `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
// Initialize Multi-Selects
|
||||
// Инициализация мультиселектов.
|
||||
initMultiSelect('building-box', 'building-menu', 'building-text', 'building-checkboxes');
|
||||
initMultiSelect('capacity-box', 'capacity-menu', 'capacity-text', 'capacity-checkboxes');
|
||||
initMultiSelect('equipment-box', 'equipment-menu', 'equipment-text', 'equipment-checkboxes');
|
||||
|
||||
// Populate Filters with Mock/Initial Data
|
||||
// Заполнение фильтров начальными данными.
|
||||
populateFilters();
|
||||
|
||||
// Render Mock Data for the Grid based on the UI requested layout
|
||||
// Отрисовка mock-данных в нужной ориентации сетки.
|
||||
renderMockGrid();
|
||||
}
|
||||
|
||||
function populateFilters() {
|
||||
// Buildings
|
||||
// Корпуса.
|
||||
const buildingsContainer = document.getElementById('building-checkboxes');
|
||||
const buildings = [
|
||||
{ id: 1, name: "Корпус 1 (Главный)" },
|
||||
@@ -39,7 +39,7 @@ function populateFilters() {
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
// Capacities
|
||||
// Вместимость.
|
||||
const capacityContainer = document.getElementById('capacity-checkboxes');
|
||||
const capacities = [
|
||||
{ id: 'small', name: "До 30 мест" },
|
||||
@@ -55,7 +55,7 @@ function populateFilters() {
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
// Equipment
|
||||
// Оборудование.
|
||||
const equipmentContainer = document.getElementById('equipment-checkboxes');
|
||||
const equipmentList = [
|
||||
{ id: 1, name: "Проектор" },
|
||||
@@ -73,7 +73,7 @@ function populateFilters() {
|
||||
}
|
||||
|
||||
function renderMockGrid() {
|
||||
// In future this will be loaded from API
|
||||
// В будущем данные будут загружаться из API.
|
||||
const timeslots = [
|
||||
"8:00-9:30",
|
||||
"9:40-11:10",
|
||||
@@ -89,7 +89,7 @@ function renderMockGrid() {
|
||||
"201", "202", "204", "205", "206", "207", "208"
|
||||
];
|
||||
|
||||
// Mock schedule data mapped by room and time
|
||||
// Mock-расписание по аудитории и времени.
|
||||
// Ключ: "roomId_timeSlotId", значение: объект занятия
|
||||
const mockSchedule = {
|
||||
"201_8:00-9:30": { subject: "Физика", group: "ИБ-41м", teacher: "Атлетов А.Р." },
|
||||
@@ -105,37 +105,43 @@ function renderMockGrid() {
|
||||
"205_9:40-11:10": { subject: "Организация аудита ИБ", group: "ИБ-41м", teacher: "Таныгин М.О." },
|
||||
};
|
||||
|
||||
// Render Headers
|
||||
// В столбцах отображаются временные слоты, в строках — аудитории.
|
||||
const headerRow = document.getElementById('workload-header-row');
|
||||
// Start after the first fixed cell (which is already in HTML)
|
||||
headerRow.innerHTML = `
|
||||
<th class="top-left-cell">
|
||||
<span class="top-label">Время</span>
|
||||
<span class="bottom-label">Аудитория</span>
|
||||
</th>
|
||||
`;
|
||||
|
||||
auditoriums.forEach(room => {
|
||||
timeslots.forEach(time => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = room;
|
||||
th.textContent = time;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
// Render Body Rows
|
||||
// Строки таблицы.
|
||||
const tbody = document.getElementById('workload-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
timeslots.forEach((time) => {
|
||||
auditoriums.forEach((room) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Add Time Cell
|
||||
const tdTime = document.createElement('td');
|
||||
tdTime.className = 'time-cell';
|
||||
tdTime.textContent = time;
|
||||
tr.appendChild(tdTime);
|
||||
// Ячейка аудитории.
|
||||
const tdRoom = document.createElement('td');
|
||||
tdRoom.className = 'axis-cell';
|
||||
tdRoom.textContent = room;
|
||||
tr.appendChild(tdRoom);
|
||||
|
||||
// Add Room Cells for this Time
|
||||
auditoriums.forEach(room => {
|
||||
// Ячейки временных слотов для аудитории.
|
||||
timeslots.forEach(time => {
|
||||
const td = document.createElement('td');
|
||||
|
||||
const scheduleKey = `${room}_${time}`;
|
||||
const lesson = mockSchedule[scheduleKey];
|
||||
|
||||
if (lesson) {
|
||||
// Render lesson card
|
||||
// Карточка занятия.
|
||||
td.innerHTML = `
|
||||
<div class="lesson-card">
|
||||
<div class="lesson-subject">${lesson.subject}</div>
|
||||
|
||||
@@ -7,15 +7,22 @@ export async function initDepartmentsData() {
|
||||
|
||||
const createDeptForm = document.getElementById('create-department-form');
|
||||
const createSpecForm = document.getElementById('create-specialty-form');
|
||||
const createProfileForm = document.getElementById('create-profile-form');
|
||||
const profileSpecialtySelect = document.getElementById('profile-specialty');
|
||||
const profilesTbody = document.getElementById('profiles-tbody');
|
||||
const editDeptForm = document.getElementById('edit-department-form');
|
||||
const editSpecForm = document.getElementById('edit-specialty-form');
|
||||
const editProfileForm = document.getElementById('edit-profile-form');
|
||||
const editDeptModal = document.getElementById('modal-edit-department');
|
||||
const editSpecModal = document.getElementById('modal-edit-specialty');
|
||||
const editProfileModal = document.getElementById('modal-edit-profile');
|
||||
const editDeptClose = document.getElementById('modal-edit-department-close');
|
||||
const editSpecClose = document.getElementById('modal-edit-specialty-close');
|
||||
const editProfileClose = document.getElementById('modal-edit-profile-close');
|
||||
|
||||
let departments = [];
|
||||
let specialties = [];
|
||||
let profiles = [];
|
||||
|
||||
async function loadData() {
|
||||
// Load Departments
|
||||
@@ -30,8 +37,11 @@ export async function initDepartmentsData() {
|
||||
try {
|
||||
specialties = await api.get('/api/specialties');
|
||||
renderSpecialties();
|
||||
renderProfileSpecialtySelect();
|
||||
await loadProfiles();
|
||||
} catch (e) {
|
||||
specTbody.innerHTML = '<tr><td colspan="4" class="loading-row">-</td></tr>';
|
||||
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">-</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +81,52 @@ export async function initDepartmentsData() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderProfileSpecialtySelect() {
|
||||
const current = profileSpecialtySelect.value;
|
||||
profileSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||
specialties.map(s => `<option value="${s.id}">${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)} — ${escapeHtml(s.specialityName || s.name)}</option>`).join('');
|
||||
if (current && specialties.some(s => String(s.id) === String(current))) {
|
||||
profileSpecialtySelect.value = current;
|
||||
} else if (specialties.length) {
|
||||
profileSpecialtySelect.value = String(specialties[0].id);
|
||||
}
|
||||
syncSelect(profileSpecialtySelect);
|
||||
}
|
||||
|
||||
async function loadProfiles() {
|
||||
const specialtyId = profileSpecialtySelect.value;
|
||||
if (!specialtyId) {
|
||||
profiles = [];
|
||||
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Выберите специальность</td></tr>';
|
||||
return;
|
||||
}
|
||||
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
profiles = await api.get(`/api/specialties/${specialtyId}/profiles`);
|
||||
renderProfiles();
|
||||
} catch (error) {
|
||||
profilesTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderProfiles() {
|
||||
if (!profiles.length) {
|
||||
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Профили не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
profilesTbody.innerHTML = profiles.map(profile => `
|
||||
<tr>
|
||||
<td>${profile.id}</td>
|
||||
<td>${escapeHtml(profile.name)}</td>
|
||||
<td>${escapeHtml(profile.description || '-')}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-profile" data-id="${profile.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-profile" data-id="${profile.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
createDeptForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-dept-alert');
|
||||
@@ -141,6 +197,59 @@ export async function initDepartmentsData() {
|
||||
}
|
||||
});
|
||||
|
||||
profileSpecialtySelect.addEventListener('change', loadProfiles);
|
||||
|
||||
createProfileForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-profile-alert');
|
||||
const specialtyId = profileSpecialtySelect.value;
|
||||
const name = document.getElementById('profile-name').value.trim();
|
||||
const description = document.getElementById('profile-description').value.trim();
|
||||
|
||||
if (!specialtyId || !name) {
|
||||
showAlert('create-profile-alert', 'Выберите специальность и заполните название профиля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.post(`/api/specialties/${specialtyId}/profiles`, { name, description });
|
||||
showAlert('create-profile-alert', `Профиль "${name}" создан`, 'success');
|
||||
createProfileForm.reset();
|
||||
profileSpecialtySelect.value = specialtyId;
|
||||
syncSelect(profileSpecialtySelect);
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
showAlert('create-profile-alert', error.message || 'Ошибка создания профиля', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
profilesTbody.addEventListener('click', async (e) => {
|
||||
const editBtn = e.target.closest('.btn-edit-profile');
|
||||
const deleteBtn = e.target.closest('.btn-delete-profile');
|
||||
|
||||
if (editBtn) {
|
||||
const profile = profiles.find(item => item.id == editBtn.dataset.id);
|
||||
if (!profile) return;
|
||||
document.getElementById('edit-profile-id').value = profile.id;
|
||||
document.getElementById('edit-profile-name').value = profile.name || '';
|
||||
document.getElementById('edit-profile-description').value = profile.description || '';
|
||||
hideAlert('edit-profile-alert');
|
||||
editProfileModal.classList.add('open');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteBtn) {
|
||||
if (!confirm('Удалить профиль обучения?')) return;
|
||||
try {
|
||||
await api.delete(`/api/specialties/${profileSpecialtySelect.value}/profiles/${deleteBtn.dataset.id}`);
|
||||
showAlert('create-profile-alert', 'Профиль удалён', 'success');
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
showAlert('create-profile-alert', error.message || 'Ошибка удаления профиля', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
specTbody.addEventListener('click', async (e) => {
|
||||
const editBtn = e.target.closest('.btn-edit-specialty');
|
||||
const deleteBtn = e.target.closest('.btn-delete-specialty');
|
||||
@@ -213,14 +322,45 @@ export async function initDepartmentsData() {
|
||||
}
|
||||
});
|
||||
|
||||
editProfileForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('edit-profile-alert');
|
||||
const specialtyId = profileSpecialtySelect.value;
|
||||
const id = document.getElementById('edit-profile-id').value;
|
||||
const name = document.getElementById('edit-profile-name').value.trim();
|
||||
const description = document.getElementById('edit-profile-description').value.trim();
|
||||
|
||||
if (!specialtyId || !id || !name) {
|
||||
showAlert('edit-profile-alert', 'Заполните название профиля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.put(`/api/specialties/${specialtyId}/profiles/${id}`, { id: Number(id), name, description });
|
||||
editProfileModal.classList.remove('open');
|
||||
showAlert('create-profile-alert', `Профиль "${name}" обновлён`, 'success');
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
showAlert('edit-profile-alert', error.message || 'Ошибка обновления профиля', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
editDeptClose.addEventListener('click', () => editDeptModal.classList.remove('open'));
|
||||
editSpecClose.addEventListener('click', () => editSpecModal.classList.remove('open'));
|
||||
editProfileClose.addEventListener('click', () => editProfileModal.classList.remove('open'));
|
||||
editDeptModal.addEventListener('click', (e) => {
|
||||
if (e.target === editDeptModal) editDeptModal.classList.remove('open');
|
||||
});
|
||||
editSpecModal.addEventListener('click', (e) => {
|
||||
if (e.target === editSpecModal) editSpecModal.classList.remove('open');
|
||||
});
|
||||
editProfileModal.addEventListener('click', (e) => {
|
||||
if (e.target === editProfileModal) editProfileModal.classList.remove('open');
|
||||
});
|
||||
|
||||
await loadData();
|
||||
|
||||
function syncSelect(select) {
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
460
frontend/admin/js/views/groups.js
Executable file → Normal file
460
frontend/admin/js/views/groups.js
Executable file → Normal file
@@ -8,105 +8,408 @@ export async function initGroups() {
|
||||
const newGroupEfSelect = document.getElementById('new-group-ef');
|
||||
const newGroupDepartmentSelect = document.getElementById('new-group-department');
|
||||
const newGroupSpecialitySelect = document.getElementById('new-group-speciality-code');
|
||||
const newGroupProfileSelect = document.getElementById('new-group-profile');
|
||||
const filterEfSelect = document.getElementById('filter-ef');
|
||||
const modalEditGroup = document.getElementById('modal-edit-group');
|
||||
const modalEditGroupClose = document.getElementById('modal-edit-group-close');
|
||||
const editGroupForm = document.getElementById('edit-group-form');
|
||||
const editGroupEfSelect = document.getElementById('edit-group-ef');
|
||||
const editGroupDepartmentSelect = document.getElementById('edit-group-department');
|
||||
const editGroupSpecialitySelect = document.getElementById('edit-group-speciality-code');
|
||||
const editGroupProfileSelect = document.getElementById('edit-group-profile');
|
||||
const calendarForm = document.getElementById('group-calendar-form');
|
||||
const calendarGroupSelect = document.getElementById('calendar-group');
|
||||
const calendarYearSelect = document.getElementById('calendar-year');
|
||||
const calendarSelect = document.getElementById('calendar-id');
|
||||
const calendarAssignmentsTbody = document.getElementById('group-calendar-tbody');
|
||||
|
||||
let allGroups = [];
|
||||
let educationForms = [];
|
||||
let departments = [];
|
||||
let specialties = [];
|
||||
let profilesBySpecialty = new Map();
|
||||
let academicYears = [];
|
||||
let calendars = [];
|
||||
let currentAssignments = [];
|
||||
|
||||
bindEvents();
|
||||
await loadInitialData();
|
||||
|
||||
function bindEvents() {
|
||||
filterEfSelect.addEventListener('change', applyGroupFilter);
|
||||
newGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value));
|
||||
editGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value));
|
||||
calendarGroupSelect.addEventListener('change', () => {
|
||||
populateCalendarSelect();
|
||||
loadAssignments();
|
||||
});
|
||||
calendarYearSelect.addEventListener('change', populateCalendarSelect);
|
||||
createGroupForm.addEventListener('submit', createGroup);
|
||||
editGroupForm.addEventListener('submit', updateGroup);
|
||||
calendarForm.addEventListener('submit', saveAssignment);
|
||||
groupsTbody.addEventListener('click', handleGroupTableClick);
|
||||
calendarAssignmentsTbody.addEventListener('click', handleAssignmentTableClick);
|
||||
modalEditGroupClose.addEventListener('click', () => modalEditGroup.classList.remove('open'));
|
||||
modalEditGroup.addEventListener('click', (event) => {
|
||||
if (event.target === modalEditGroup) {
|
||||
modalEditGroup.classList.remove('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
[educationForms, departments, specialties] = await Promise.all([
|
||||
[educationForms, departments, specialties, academicYears, calendars] = await Promise.all([
|
||||
fetchEducationForms(),
|
||||
api.get('/api/departments'),
|
||||
api.get('/api/specialties')
|
||||
api.get('/api/specialties'),
|
||||
api.get('/api/admin/calendar/years'),
|
||||
api.get('/api/admin/academic-calendars')
|
||||
]);
|
||||
await loadProfiles();
|
||||
populateEfSelects(educationForms);
|
||||
populateDepartmentSelect(departments);
|
||||
populateSpecialitySelect(specialties);
|
||||
populateDepartmentSelects(departments);
|
||||
populateSpecialitySelects(specialties);
|
||||
populateYearSelect();
|
||||
populateCalendarSelect();
|
||||
await loadGroups();
|
||||
} catch (e) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки данных</td></tr>';
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки данных: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfiles() {
|
||||
const pairs = await Promise.all(specialties.map(async (speciality) => {
|
||||
const profiles = await api.get(`/api/specialties/${speciality.id}/profiles`);
|
||||
return [String(speciality.id), profiles];
|
||||
}));
|
||||
profilesBySpecialty = new Map(pairs);
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
allGroups = await api.get('/api/groups');
|
||||
applyGroupFilter();
|
||||
} catch (e) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
populateGroupSelect();
|
||||
await loadAssignments();
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyGroupFilter() {
|
||||
const filterId = filterEfSelect.value;
|
||||
const filtered = filterId
|
||||
? allGroups.filter(g => g.educationFormId == filterId)
|
||||
? allGroups.filter(group => group.educationFormId == filterId)
|
||||
: allGroups;
|
||||
renderGroups(filtered);
|
||||
}
|
||||
|
||||
filterEfSelect.addEventListener('change', applyGroupFilter);
|
||||
function renderGroups(groups) {
|
||||
if (!groups || !groups.length) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет групп</td></tr>';
|
||||
return;
|
||||
}
|
||||
groupsTbody.innerHTML = groups.map(group => `
|
||||
<tr>
|
||||
<td>${group.id}</td>
|
||||
<td>${escapeHtml(group.name)}</td>
|
||||
<td>${escapeHtml(String(group.groupSize))}</td>
|
||||
<td><span class="badge badge-ef">${escapeHtml(group.educationFormName)}</span></td>
|
||||
<td>${escapeHtml(departmentLabel(group.departmentId))}</td>
|
||||
<td>${group.course || '-'}</td>
|
||||
<td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td>
|
||||
<td>${escapeHtml(group.specialtyProfileName || '-')}</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="btn-edit-classroom btn-edit-group" data-id="${group.id}">Изменить</button>
|
||||
<button class="btn-delete" data-id="${group.id}" style="margin-left: 0.5rem;">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function populateEfSelects(forms) {
|
||||
// Group creation select
|
||||
const currentVal = newGroupEfSelect.value;
|
||||
newGroupEfSelect.innerHTML = forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
if (currentVal && forms.find(f => f.id == currentVal)) {
|
||||
newGroupEfSelect.value = currentVal;
|
||||
}
|
||||
syncSelects(newGroupEfSelect);
|
||||
|
||||
// Filter select
|
||||
populateEfSelect(newGroupEfSelect, forms);
|
||||
populateEfSelect(editGroupEfSelect, forms);
|
||||
const currentFilter = filterEfSelect.value;
|
||||
filterEfSelect.innerHTML = '<option value="">Все формы</option>' +
|
||||
forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
forms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
|
||||
if (currentFilter) filterEfSelect.value = currentFilter;
|
||||
syncSelects(filterEfSelect);
|
||||
}
|
||||
|
||||
function populateDepartmentSelect(items) {
|
||||
const currentVal = newGroupDepartmentSelect.value;
|
||||
if (!items || !items.length) {
|
||||
newGroupDepartmentSelect.innerHTML = '<option value="">Нет кафедр</option>';
|
||||
return;
|
||||
function populateEfSelect(select, forms) {
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = forms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
|
||||
if (currentVal && forms.find(form => form.id == currentVal)) {
|
||||
select.value = currentVal;
|
||||
}
|
||||
syncSelects(select);
|
||||
}
|
||||
|
||||
newGroupDepartmentSelect.innerHTML = '<option value="">Выберите кафедру</option>' +
|
||||
function populateDepartmentSelects(items) {
|
||||
populateDepartmentSelect(newGroupDepartmentSelect, items);
|
||||
populateDepartmentSelect(editGroupDepartmentSelect, items);
|
||||
}
|
||||
|
||||
function populateDepartmentSelect(select, items) {
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = '<option value="">Выберите кафедру</option>' +
|
||||
items.map(department => {
|
||||
const name = department.departmentName || department.name || 'Без названия';
|
||||
const code = department.departmentCode || department.code || department.id;
|
||||
return `<option value="${department.id}">${escapeHtml(name)} (${escapeHtml(String(code))})</option>`;
|
||||
}).join('');
|
||||
if (currentVal && items.find(department => department.id == currentVal)) {
|
||||
newGroupDepartmentSelect.value = currentVal;
|
||||
select.value = currentVal;
|
||||
}
|
||||
syncSelects(newGroupDepartmentSelect);
|
||||
syncSelects(select);
|
||||
}
|
||||
|
||||
function populateSpecialitySelect(items) {
|
||||
const currentVal = newGroupSpecialitySelect.value;
|
||||
if (!items || !items.length) {
|
||||
newGroupSpecialitySelect.innerHTML = '<option value="">Нет специальностей</option>';
|
||||
function populateSpecialitySelects(items) {
|
||||
populateSpecialitySelect(newGroupSpecialitySelect, items);
|
||||
populateSpecialitySelect(editGroupSpecialitySelect, items);
|
||||
populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value);
|
||||
populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value);
|
||||
}
|
||||
|
||||
function populateSpecialitySelect(select, items) {
|
||||
const currentVal = select.value;
|
||||
select.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||
items.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality.id))}</option>`).join('');
|
||||
if (currentVal && items.find(speciality => speciality.id == currentVal)) {
|
||||
select.value = currentVal;
|
||||
}
|
||||
syncSelects(select);
|
||||
}
|
||||
|
||||
function populateProfileSelect(select, specialtyId, selectedProfileId = null) {
|
||||
const profiles = profilesBySpecialty.get(String(specialtyId)) || [];
|
||||
select.innerHTML = profiles.length
|
||||
? '<option value="">Выберите профиль</option>' + profiles.map(profile =>
|
||||
`<option value="${profile.id}">${escapeHtml(profile.name)}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет профилей</option>';
|
||||
if (selectedProfileId && profiles.some(profile => String(profile.id) === String(selectedProfileId))) {
|
||||
select.value = selectedProfileId;
|
||||
}
|
||||
syncSelects(select);
|
||||
}
|
||||
|
||||
function populateYearSelect() {
|
||||
calendarYearSelect.innerHTML = '<option value="">Выберите учебный год</option>' +
|
||||
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
||||
if (!calendarYearSelect.value && academicYears.length) {
|
||||
calendarYearSelect.value = String(academicYears[0].id);
|
||||
}
|
||||
syncSelects(calendarYearSelect);
|
||||
}
|
||||
|
||||
function populateGroupSelect() {
|
||||
const current = calendarGroupSelect.value;
|
||||
calendarGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
|
||||
allGroups.map(group => `<option value="${group.id}">${escapeHtml(group.name)}</option>`).join('');
|
||||
if (current && allGroups.some(group => String(group.id) === String(current))) {
|
||||
calendarGroupSelect.value = current;
|
||||
} else if (allGroups.length) {
|
||||
calendarGroupSelect.value = String(allGroups[0].id);
|
||||
}
|
||||
syncSelects(calendarGroupSelect);
|
||||
}
|
||||
|
||||
function populateCalendarSelect() {
|
||||
const group = selectedCalendarGroup();
|
||||
const yearId = calendarYearSelect.value;
|
||||
const matching = group && yearId
|
||||
? calendars.filter(calendar =>
|
||||
String(calendar.academicYearId) === String(yearId)
|
||||
&& String(calendar.specialtyId) === String(group.specialtyId || group.specialityCode)
|
||||
&& String(calendar.specialtyProfileId) === String(group.specialtyProfileId)
|
||||
)
|
||||
: [];
|
||||
calendarSelect.innerHTML = matching.length
|
||||
? '<option value="">Выберите график</option>' + matching.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет подходящих графиков</option>';
|
||||
syncSelects(calendarSelect);
|
||||
}
|
||||
|
||||
async function createGroup(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('create-group-alert');
|
||||
const payload = readGroupPayload('new');
|
||||
if (!payload) return;
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/groups', payload);
|
||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || payload.name)}" создана`, 'success');
|
||||
createGroupForm.reset();
|
||||
syncSelects(newGroupEfSelect, newGroupDepartmentSelect, newGroupSpecialitySelect, newGroupProfileSelect);
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
showAlert('create-group-alert', error.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGroup(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('edit-group-alert');
|
||||
const id = document.getElementById('edit-group-id').value;
|
||||
const payload = readGroupPayload('edit');
|
||||
if (!payload) return;
|
||||
|
||||
try {
|
||||
const data = await api.put('/api/groups/' + id, payload);
|
||||
modalEditGroup.classList.remove('open');
|
||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || payload.name)}" обновлена`, 'success');
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
showAlert('edit-group-alert', error.message || 'Ошибка обновления', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function readGroupPayload(prefix) {
|
||||
const isEdit = prefix === 'edit';
|
||||
const alertId = isEdit ? 'edit-group-alert' : 'create-group-alert';
|
||||
const name = document.getElementById(`${prefix}-group-name`).value.trim();
|
||||
const groupSize = document.getElementById(`${prefix}-group-size`).value;
|
||||
const educationFormId = (isEdit ? editGroupEfSelect : newGroupEfSelect).value;
|
||||
const departmentId = (isEdit ? editGroupDepartmentSelect : newGroupDepartmentSelect).value;
|
||||
const yearStartStudy = document.getElementById(`${prefix}-group-yearStartStudy`).value;
|
||||
const specialtyId = (isEdit ? editGroupSpecialitySelect : newGroupSpecialitySelect).value;
|
||||
const specialtyProfileId = (isEdit ? editGroupProfileSelect : newGroupProfileSelect).value;
|
||||
|
||||
if (!name) { showAlert(alertId, 'Введите название группы', 'error'); return null; }
|
||||
if (!groupSize) { showAlert(alertId, 'Введите размер группы', 'error'); return null; }
|
||||
if (!educationFormId) { showAlert(alertId, 'Выберите форму обучения', 'error'); return null; }
|
||||
if (!departmentId) { showAlert(alertId, 'Выберите кафедру', 'error'); return null; }
|
||||
if (!yearStartStudy) { showAlert(alertId, 'Введите год начала обучения', 'error'); return null; }
|
||||
if (!specialtyId) { showAlert(alertId, 'Выберите специальность', 'error'); return null; }
|
||||
if (!specialtyProfileId) { showAlert(alertId, 'Выберите профиль обучения', 'error'); return null; }
|
||||
|
||||
return {
|
||||
name,
|
||||
groupSize: Number(groupSize),
|
||||
educationFormId: Number(educationFormId),
|
||||
departmentId: Number(departmentId),
|
||||
yearStartStudy: Number(yearStartStudy),
|
||||
specialtyId: Number(specialtyId),
|
||||
specialtyProfileId: Number(specialtyProfileId)
|
||||
};
|
||||
}
|
||||
|
||||
async function handleGroupTableClick(event) {
|
||||
const btnDelete = event.target.closest('.btn-delete');
|
||||
const btnEdit = event.target.closest('.btn-edit-group');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
try {
|
||||
await api.delete('/api/groups/' + btnDelete.dataset.id);
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка удаления');
|
||||
}
|
||||
}
|
||||
|
||||
if (btnEdit) {
|
||||
openEditGroupModal(btnEdit.dataset.id);
|
||||
}
|
||||
}
|
||||
|
||||
function openEditGroupModal(id) {
|
||||
const group = allGroups.find(item => item.id == id);
|
||||
if (!group) {
|
||||
alert('Группа не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
newGroupSpecialitySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||
items.map(speciality => {
|
||||
const name = speciality.specialityName || speciality.name || 'Без названия';
|
||||
const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id;
|
||||
return `<option value="${speciality.id}">${escapeHtml(code)} — ${escapeHtml(name)}</option>`;
|
||||
}).join('');
|
||||
if (currentVal && items.find(speciality => speciality.id == currentVal)) {
|
||||
newGroupSpecialitySelect.value = currentVal;
|
||||
document.getElementById('edit-group-id').value = group.id;
|
||||
document.getElementById('edit-group-name').value = group.name || '';
|
||||
document.getElementById('edit-group-size').value = group.groupSize || '';
|
||||
editGroupEfSelect.value = group.educationFormId || '';
|
||||
editGroupDepartmentSelect.value = group.departmentId || '';
|
||||
document.getElementById('edit-group-yearStartStudy').value = group.yearStartStudy || '';
|
||||
editGroupSpecialitySelect.value = group.specialtyId || group.specialityCode || '';
|
||||
populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value, group.specialtyProfileId);
|
||||
syncSelects(editGroupEfSelect, editGroupDepartmentSelect, editGroupSpecialitySelect, editGroupProfileSelect);
|
||||
hideAlert('edit-group-alert');
|
||||
modalEditGroup.classList.add('open');
|
||||
}
|
||||
|
||||
async function saveAssignment(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('group-calendar-alert');
|
||||
const groupId = calendarGroupSelect.value;
|
||||
const academicYearId = calendarYearSelect.value;
|
||||
const calendarId = calendarSelect.value;
|
||||
|
||||
if (!groupId || !academicYearId || !calendarId) {
|
||||
showAlert('group-calendar-alert', 'Выберите группу, учебный год и график', 'error');
|
||||
return;
|
||||
}
|
||||
syncSelects(newGroupSpecialitySelect);
|
||||
|
||||
try {
|
||||
await api.put(`/api/groups/${groupId}/calendar-assignments`, {
|
||||
academicYearId: Number(academicYearId),
|
||||
calendarId: Number(calendarId)
|
||||
});
|
||||
showAlert('group-calendar-alert', 'Календарный график назначен', 'success');
|
||||
await loadAssignments();
|
||||
} catch (error) {
|
||||
showAlert('group-calendar-alert', error.message || 'Ошибка назначения графика', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAssignments() {
|
||||
const groupId = calendarGroupSelect.value;
|
||||
if (!groupId) {
|
||||
currentAssignments = [];
|
||||
calendarAssignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Выберите группу</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentAssignments = await api.get(`/api/groups/${groupId}/calendar-assignments`);
|
||||
renderAssignments();
|
||||
} catch (error) {
|
||||
calendarAssignmentsTbody.innerHTML = `<tr><td colspan="3" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssignments() {
|
||||
if (!currentAssignments.length) {
|
||||
calendarAssignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Графики не назначены</td></tr>';
|
||||
return;
|
||||
}
|
||||
calendarAssignmentsTbody.innerHTML = currentAssignments.map(assignment => `
|
||||
<tr>
|
||||
<td>${escapeHtml(assignment.academicYearTitle)}</td>
|
||||
<td>${escapeHtml(assignment.calendarTitle)}</td>
|
||||
<td>
|
||||
<button class="btn-delete btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function handleAssignmentTableClick(event) {
|
||||
const deleteButton = event.target.closest('.btn-delete-assignment');
|
||||
if (!deleteButton) return;
|
||||
if (!confirm('Удалить назначение графика?')) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/api/groups/${calendarGroupSelect.value}/calendar-assignments/${deleteButton.dataset.id}`);
|
||||
showAlert('group-calendar-alert', 'Назначение удалено', 'success');
|
||||
await loadAssignments();
|
||||
} catch (error) {
|
||||
showAlert('group-calendar-alert', error.message || 'Ошибка удаления назначения', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function selectedCalendarGroup() {
|
||||
return allGroups.find(group => String(group.id) === String(calendarGroupSelect.value));
|
||||
}
|
||||
|
||||
function departmentLabel(departmentId) {
|
||||
@@ -123,73 +426,6 @@ export async function initGroups() {
|
||||
return name ? `${code} — ${name}` : code;
|
||||
}
|
||||
|
||||
function renderGroups(groups) {
|
||||
if (!groups || !groups.length) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет групп</td></tr>';
|
||||
return;
|
||||
}
|
||||
groupsTbody.innerHTML = groups.map(g => `
|
||||
<tr>
|
||||
<td>${g.id}</td>
|
||||
<td>${escapeHtml(g.name)}</td>
|
||||
<td>${escapeHtml(g.groupSize)}</td>
|
||||
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
||||
<td>${escapeHtml(departmentLabel(g.departmentId))}</td>
|
||||
<td>${g.course || '-'}</td>
|
||||
<td>${escapeHtml(specialityLabel(g.specialityCode))}</td>
|
||||
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createGroupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-group-alert');
|
||||
const name = document.getElementById('new-group-name').value.trim();
|
||||
const groupSize = document.getElementById('new-group-size').value;
|
||||
const educationFormId = newGroupEfSelect.value;
|
||||
const departmentId = newGroupDepartmentSelect.value;
|
||||
const yearStartStudy = document.getElementById('new-group-yearStartStudy').value;
|
||||
const specialityCode = newGroupSpecialitySelect.value;
|
||||
|
||||
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
||||
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
|
||||
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
||||
if (!departmentId) { showAlert('create-group-alert', 'Выберите кафедру', 'error'); return; }
|
||||
if (!yearStartStudy) { showAlert('create-group-alert', 'Введите год начала обучения', 'error'); return; }
|
||||
if (!specialityCode) { showAlert('create-group-alert', 'Выберите специальность', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/groups', {
|
||||
name,
|
||||
groupSize: Number(groupSize),
|
||||
educationFormId: Number(educationFormId),
|
||||
departmentId: Number(departmentId),
|
||||
yearStartStudy: Number(yearStartStudy),
|
||||
specialityCode: Number(specialityCode)
|
||||
});
|
||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
|
||||
createGroupForm.reset();
|
||||
syncSelects(newGroupEfSelect, newGroupDepartmentSelect, newGroupSpecialitySelect);
|
||||
loadGroups();
|
||||
} catch (e) {
|
||||
showAlert('create-group-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
groupsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
try {
|
||||
await api.delete('/api/groups/' + btn.dataset.id);
|
||||
loadGroups();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
});
|
||||
|
||||
await loadInitialData();
|
||||
|
||||
function syncSelects(...selects) {
|
||||
selects.filter(Boolean).forEach(select => {
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
@@ -22,13 +22,6 @@ const SEMESTER_LABELS = {
|
||||
spring: 'весенний'
|
||||
};
|
||||
|
||||
const ACTIVITY_LABELS = {
|
||||
THEORY: 'Теория',
|
||||
EXAM: 'Экзамены',
|
||||
VACATION: 'Каникулы',
|
||||
PRACTICE: 'Практика'
|
||||
};
|
||||
|
||||
export async function initSchedule() {
|
||||
const ruleForm = document.getElementById('schedule-rule-form');
|
||||
const ruleFormTitle = document.getElementById('schedule-rule-form-title');
|
||||
@@ -74,20 +67,29 @@ export async function initSchedule() {
|
||||
const semesterEndInput = document.getElementById('semester-end');
|
||||
const semesterResetButton = document.getElementById('semester-reset');
|
||||
|
||||
const holidayForm = document.getElementById('holiday-form');
|
||||
const holidayIdInput = document.getElementById('holiday-id');
|
||||
const holidayYearSelect = document.getElementById('holiday-year');
|
||||
const holidayDateInput = document.getElementById('holiday-date');
|
||||
const holidayDescriptionInput = document.getElementById('holiday-description');
|
||||
const holidayResetButton = document.getElementById('holiday-reset');
|
||||
const holidaysTbody = document.getElementById('holidays-tbody');
|
||||
const calendarForm = document.getElementById('academic-calendar-form');
|
||||
const calendarFormTitle = document.getElementById('academic-calendar-form-title');
|
||||
const calendarIdInput = document.getElementById('academic-calendar-id');
|
||||
const calendarTitleInput = document.getElementById('academic-calendar-title');
|
||||
const calendarYearSelect = document.getElementById('academic-calendar-year');
|
||||
const calendarSpecialtySelect = document.getElementById('academic-calendar-specialty');
|
||||
const calendarProfileSelect = document.getElementById('academic-calendar-profile');
|
||||
const calendarStudyFormSelect = document.getElementById('academic-calendar-study-form');
|
||||
const calendarCourseCountInput = document.getElementById('academic-calendar-course-count');
|
||||
const calendarResetButton = document.getElementById('academic-calendar-reset');
|
||||
const calendarsRefreshButton = document.getElementById('academic-calendars-refresh');
|
||||
const calendarsTbody = document.getElementById('academic-calendars-tbody');
|
||||
|
||||
const matrixSemesterSelect = document.getElementById('matrix-semester');
|
||||
const matrixCourseInput = document.getElementById('matrix-course');
|
||||
const matrixSpecialtySelect = document.getElementById('matrix-specialty');
|
||||
const matrixLoadButton = document.getElementById('matrix-load');
|
||||
const matrixSaveButton = document.getElementById('matrix-save');
|
||||
const matrixGrid = document.getElementById('matrix-grid');
|
||||
const editorCalendarSelect = document.getElementById('calendar-editor-calendar');
|
||||
const editorLoadButton = document.getElementById('calendar-editor-load');
|
||||
const editorSaveButton = document.getElementById('calendar-editor-save');
|
||||
const fillCourseSelect = document.getElementById('calendar-fill-course');
|
||||
const fillStartInput = document.getElementById('calendar-fill-start');
|
||||
const fillEndInput = document.getElementById('calendar-fill-end');
|
||||
const fillActivitySelect = document.getElementById('calendar-fill-activity');
|
||||
const fillApplyButton = document.getElementById('calendar-fill-apply');
|
||||
const calendarGrid = document.getElementById('calendar-grid');
|
||||
const calendarTotals = document.getElementById('calendar-totals');
|
||||
|
||||
let rules = [];
|
||||
let subjects = [];
|
||||
@@ -99,15 +101,19 @@ export async function initSchedule() {
|
||||
let academicYears = [];
|
||||
let semesters = [];
|
||||
let specialties = [];
|
||||
let holidays = [];
|
||||
let profilesBySpecialty = new Map();
|
||||
let studyForms = [];
|
||||
let activityTypes = [];
|
||||
let calendars = [];
|
||||
let currentGridRows = [];
|
||||
|
||||
bindEvents();
|
||||
|
||||
try {
|
||||
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears()]);
|
||||
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears(), loadStudyForms(), loadActivityTypes()]);
|
||||
renderRuleGroups([]);
|
||||
renderRuleSlots([{}]);
|
||||
await Promise.all([loadRules(), loadHolidays()]);
|
||||
await Promise.all([loadRules(), loadCalendars()]);
|
||||
} catch (error) {
|
||||
showAlert('schedule-rule-alert', error.message || 'Ошибка загрузки данных расписания', 'error');
|
||||
}
|
||||
@@ -116,15 +122,17 @@ export async function initSchedule() {
|
||||
refreshRulesButton?.addEventListener('click', loadRules);
|
||||
refreshSlotsButton?.addEventListener('click', loadTimeSlots);
|
||||
academicYearsRefreshButton?.addEventListener('click', loadYears);
|
||||
calendarsRefreshButton?.addEventListener('click', loadCalendars);
|
||||
ruleResetButton?.addEventListener('click', resetRuleForm);
|
||||
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
|
||||
timeSlotResetButton?.addEventListener('click', resetTimeSlotForm);
|
||||
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
|
||||
semesterResetButton?.addEventListener('click', resetSemesterForm);
|
||||
holidayResetButton?.addEventListener('click', resetHolidayForm);
|
||||
holidayYearSelect?.addEventListener('change', loadHolidays);
|
||||
matrixLoadButton?.addEventListener('click', loadMatrix);
|
||||
matrixSaveButton?.addEventListener('click', saveMatrix);
|
||||
calendarResetButton?.addEventListener('click', resetCalendarForm);
|
||||
calendarSpecialtySelect?.addEventListener('change', () => populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value));
|
||||
editorLoadButton?.addEventListener('click', loadCalendarGrid);
|
||||
editorSaveButton?.addEventListener('click', saveCalendarGrid);
|
||||
fillApplyButton?.addEventListener('click', applyCalendarFill);
|
||||
|
||||
ruleSlotsContainer.addEventListener('click', (event) => {
|
||||
const removeButton = event.target.closest('.schedule-slot-remove');
|
||||
@@ -134,16 +142,22 @@ export async function initSchedule() {
|
||||
renderRuleSlots(slots.length ? slots : [{}]);
|
||||
});
|
||||
|
||||
calendarGrid.addEventListener('change', (event) => {
|
||||
if (event.target.matches('.calendar-day-activity')) {
|
||||
renderCalendarTotals();
|
||||
}
|
||||
});
|
||||
|
||||
ruleForm.addEventListener('submit', saveRule);
|
||||
timeSlotForm.addEventListener('submit', saveTimeSlot);
|
||||
academicYearForm.addEventListener('submit', saveAcademicYear);
|
||||
semesterForm.addEventListener('submit', saveSemester);
|
||||
holidayForm.addEventListener('submit', saveHoliday);
|
||||
calendarForm.addEventListener('submit', saveCalendar);
|
||||
|
||||
rulesTbody.addEventListener('click', handleRuleTableClick);
|
||||
timeSlotsTbody.addEventListener('click', handleTimeSlotTableClick);
|
||||
academicYearsTbody.addEventListener('click', handleAcademicYearTableClick);
|
||||
holidaysTbody.addEventListener('click', handleHolidayTableClick);
|
||||
calendarsTbody.addEventListener('click', handleCalendarTableClick);
|
||||
}
|
||||
|
||||
async function loadBaseLists() {
|
||||
@@ -155,11 +169,19 @@ export async function initSchedule() {
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/specialties')
|
||||
]);
|
||||
|
||||
await loadProfiles();
|
||||
populateSubjectSelects();
|
||||
populateSpecialtySelects();
|
||||
}
|
||||
|
||||
async function loadProfiles() {
|
||||
const pairs = await Promise.all(specialties.map(async (speciality) => {
|
||||
const profiles = await api.get(`/api/specialties/${speciality.id}/profiles`);
|
||||
return [String(speciality.id), profiles];
|
||||
}));
|
||||
profilesBySpecialty = new Map(pairs);
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
@@ -171,11 +193,10 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function renderRules() {
|
||||
if (!rules || !rules.length) {
|
||||
if (!rules.length) {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Правила расписания не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
rulesTbody.innerHTML = rules.map(rule => `
|
||||
<tr>
|
||||
<td>${rule.id}</td>
|
||||
@@ -195,7 +216,6 @@ export async function initSchedule() {
|
||||
|
||||
function renderRuleSlotsSummary(slots) {
|
||||
if (!slots || !slots.length) return '<span class="muted">Нет слотов</span>';
|
||||
|
||||
return slots.map(slot => {
|
||||
const parity = PARITY_LABELS[slot.parity] || slot.parity || '-';
|
||||
const parts = [
|
||||
@@ -237,7 +257,6 @@ export async function initSchedule() {
|
||||
async function saveRule(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('schedule-rule-alert');
|
||||
|
||||
try {
|
||||
const payload = buildRulePayload();
|
||||
const id = ruleIdInput.value;
|
||||
@@ -256,26 +275,12 @@ export async function initSchedule() {
|
||||
const groupIds = Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(input => Number(input.value));
|
||||
const slots = readRuleSlots(true);
|
||||
|
||||
if (!ruleSubjectSelect.value) {
|
||||
throw new Error('Выберите дисциплину');
|
||||
}
|
||||
if (!ruleSemesterSelect.value) {
|
||||
throw new Error('Выберите семестр');
|
||||
}
|
||||
if (!ruleActiveFromInput.value) {
|
||||
throw new Error('Укажите дату начала правила');
|
||||
}
|
||||
if (!ruleHoursInput.value || Number(ruleHoursInput.value) <= 0) {
|
||||
throw new Error('Укажите количество академических часов');
|
||||
}
|
||||
if (!groupIds.length) {
|
||||
throw new Error('Выберите хотя бы одну группу');
|
||||
}
|
||||
if (!slots.length) {
|
||||
throw new Error('Добавьте хотя бы один слот занятия');
|
||||
}
|
||||
|
||||
if (!ruleSubjectSelect.value) throw new Error('Выберите дисциплину');
|
||||
if (!ruleSemesterSelect.value) throw new Error('Выберите семестр');
|
||||
if (!ruleActiveFromInput.value) throw new Error('Укажите дату начала правила');
|
||||
if (!ruleHoursInput.value || Number(ruleHoursInput.value) <= 0) throw new Error('Укажите количество академических часов');
|
||||
if (!groupIds.length) throw new Error('Выберите хотя бы одну группу');
|
||||
if (!slots.length) throw new Error('Добавьте хотя бы один слот занятия');
|
||||
return {
|
||||
id: ruleIdInput.value ? Number(ruleIdInput.value) : null,
|
||||
subjectId: Number(ruleSubjectSelect.value),
|
||||
@@ -316,7 +321,6 @@ export async function initSchedule() {
|
||||
ruleGroupsContainer.innerHTML = '<div class="loading-row">Нет групп</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = new Set((selectedIds || []).map(String));
|
||||
ruleGroupsContainer.innerHTML = groups.map(group => `
|
||||
<label class="checkbox-item">
|
||||
@@ -330,38 +334,13 @@ export async function initSchedule() {
|
||||
function renderRuleSlots(slots) {
|
||||
ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => `
|
||||
<div class="schedule-slot-row" data-index="${index}">
|
||||
<div class="form-group">
|
||||
<label>День</label>
|
||||
<select class="slot-day" required>${options(DAY_OPTIONS, slot.dayOfWeek)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Чётность</label>
|
||||
<select class="slot-parity" required>
|
||||
${options(Object.entries(PARITY_LABELS).map(([value, label]) => ({ value, label })), slot.parity || 'BOTH')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Пара</label>
|
||||
<select class="slot-time" required>${options(timeSlots.map(toTimeSlotOption), slot.timeSlotId)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Преподаватель</label>
|
||||
<select class="slot-teacher" required>${options(teachers.map(toTeacherOption), slot.teacherId)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Аудитория</label>
|
||||
<select class="slot-classroom" required>${options(classrooms.map(toClassroomOption), slot.classroomId)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Тип</label>
|
||||
<select class="slot-lesson-type" required>${options(lessonTypes.map(toLessonTypeOption), slot.lessonTypeId)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Формат</label>
|
||||
<select class="slot-format" required>
|
||||
${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>День</label><select class="slot-day" required>${options(DAY_OPTIONS, slot.dayOfWeek)}</select></div>
|
||||
<div class="form-group"><label>Чётность</label><select class="slot-parity" required>${options(Object.entries(PARITY_LABELS).map(([value, label]) => ({ value, label })), slot.parity || 'BOTH')}</select></div>
|
||||
<div class="form-group"><label>Пара</label><select class="slot-time" required>${options(timeSlots.map(toTimeSlotOption), slot.timeSlotId)}</select></div>
|
||||
<div class="form-group"><label>Преподаватель</label><select class="slot-teacher" required>${options(teachers.map(toTeacherOption), slot.teacherId)}</select></div>
|
||||
<div class="form-group"><label>Аудитория</label><select class="slot-classroom" required>${options(classrooms.map(toClassroomOption), slot.classroomId)}</select></div>
|
||||
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(lessonTypes.map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
|
||||
<div class="form-group"><label>Формат</label><select class="slot-format" required>${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}</select></div>
|
||||
<button type="button" class="btn-delete schedule-slot-remove" data-index="${index}">Удалить</button>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -381,7 +360,6 @@ export async function initSchedule() {
|
||||
lessonTypeId: Number(row.querySelector('.slot-lesson-type').value),
|
||||
lessonFormat: row.querySelector('.slot-format').value
|
||||
};
|
||||
|
||||
if (validate && (!slot.dayOfWeek || !slot.parity || !slot.timeSlotId
|
||||
|| !slot.teacherId || !slot.classroomId || !slot.lessonTypeId || !slot.lessonFormat)) {
|
||||
throw new Error(`Заполните слот ${index + 1}`);
|
||||
@@ -401,11 +379,10 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function renderTimeSlots() {
|
||||
if (!timeSlots || !timeSlots.length) {
|
||||
if (!timeSlots.length) {
|
||||
timeSlotsTbody.innerHTML = '<tr><td colspan="5" class="loading-row">Сетка пар не настроена</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
timeSlotsTbody.innerHTML = timeSlots.map(slot => `
|
||||
<tr>
|
||||
<td>${slot.orderNumber}</td>
|
||||
@@ -423,7 +400,6 @@ export async function initSchedule() {
|
||||
async function saveTimeSlot(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('time-slot-alert');
|
||||
|
||||
const payload = {
|
||||
id: timeSlotIdInput.value ? Number(timeSlotIdInput.value) : null,
|
||||
orderNumber: Number(timeSlotOrderInput.value),
|
||||
@@ -431,10 +407,9 @@ export async function initSchedule() {
|
||||
endTime: timeSlotEndInput.value,
|
||||
durationMinutes: timeSlotDurationInput.value ? Number(timeSlotDurationInput.value) : null
|
||||
};
|
||||
|
||||
try {
|
||||
if (!payload.orderNumber || !payload.startTime || !payload.endTime) {
|
||||
throw new Error('Заполните номер пары, начало и окончание');
|
||||
throw new Error('Заполните номер пары и время');
|
||||
}
|
||||
const id = timeSlotIdInput.value;
|
||||
await (id
|
||||
@@ -443,7 +418,6 @@ export async function initSchedule() {
|
||||
showAlert('time-slot-alert', 'Временной слот сохранён', 'success');
|
||||
resetTimeSlotForm();
|
||||
await loadTimeSlots();
|
||||
renderRuleSlots(readRuleSlots(false));
|
||||
} catch (error) {
|
||||
showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error');
|
||||
}
|
||||
@@ -452,7 +426,6 @@ export async function initSchedule() {
|
||||
async function handleTimeSlotTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-time-slot');
|
||||
const deleteButton = event.target.closest('.btn-delete-time-slot');
|
||||
|
||||
if (editButton) {
|
||||
const slot = timeSlots.find(item => item.id == editButton.dataset.id);
|
||||
if (!slot) return;
|
||||
@@ -466,7 +439,6 @@ export async function initSchedule() {
|
||||
timeSlotForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteButton) {
|
||||
if (!confirm('Удалить временной слот?')) return;
|
||||
try {
|
||||
@@ -500,15 +472,14 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function renderAcademicYears() {
|
||||
if (!academicYears || !academicYears.length) {
|
||||
if (!academicYears.length) {
|
||||
academicYearsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Учебные годы не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
academicYearsTbody.innerHTML = academicYears.map(year => `
|
||||
<tr>
|
||||
<td>${escapeHtml(year.title)}</td>
|
||||
<td>${escapeHtml(year.startDate)} — ${escapeHtml(year.endDate)}</td>
|
||||
<td>${escapeHtml(year.startDate)} - ${escapeHtml(year.endDate)}</td>
|
||||
<td>${renderSemesterList(year.semesters || [])}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button>
|
||||
@@ -531,17 +502,13 @@ export async function initSchedule() {
|
||||
async function saveAcademicYear(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('academic-year-alert');
|
||||
|
||||
const payload = {
|
||||
title: academicYearTitleInput.value.trim(),
|
||||
startDate: academicYearStartInput.value,
|
||||
endDate: academicYearEndInput.value
|
||||
};
|
||||
|
||||
try {
|
||||
if (!payload.title || !payload.startDate || !payload.endDate) {
|
||||
throw new Error('Заполните название и даты учебного года');
|
||||
}
|
||||
if (!payload.title || !payload.startDate || !payload.endDate) throw new Error('Заполните название и даты учебного года');
|
||||
const id = academicYearIdInput.value;
|
||||
await (id
|
||||
? api.put('/api/admin/calendar/years/' + id, payload)
|
||||
@@ -549,6 +516,7 @@ export async function initSchedule() {
|
||||
showAlert('academic-year-alert', 'Учебный год сохранён', 'success');
|
||||
resetAcademicYearForm();
|
||||
await loadYears();
|
||||
await loadCalendars();
|
||||
} catch (error) {
|
||||
showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error');
|
||||
}
|
||||
@@ -558,7 +526,6 @@ export async function initSchedule() {
|
||||
const editYearButton = event.target.closest('.btn-edit-year');
|
||||
const deleteYearButton = event.target.closest('.btn-delete-year');
|
||||
const editSemesterButton = event.target.closest('.btn-edit-semester');
|
||||
|
||||
if (editYearButton) {
|
||||
const year = academicYears.find(item => item.id == editYearButton.dataset.id);
|
||||
if (!year) return;
|
||||
@@ -571,7 +538,6 @@ export async function initSchedule() {
|
||||
academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (editSemesterButton) {
|
||||
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
|
||||
if (!semester) return;
|
||||
@@ -586,13 +552,13 @@ export async function initSchedule() {
|
||||
semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteYearButton) {
|
||||
if (!confirm('Удалить учебный год вместе с семестрами?')) return;
|
||||
if (!confirm('Удалить учебный год вместе с семестрами и календарными графиками?')) return;
|
||||
try {
|
||||
await api.delete('/api/admin/calendar/years/' + deleteYearButton.dataset.id);
|
||||
showAlert('academic-year-alert', 'Учебный год удалён', 'success');
|
||||
await loadYears();
|
||||
await loadCalendars();
|
||||
} catch (error) {
|
||||
showAlert('academic-year-alert', error.message || 'Ошибка удаления учебного года', 'error');
|
||||
}
|
||||
@@ -609,18 +575,14 @@ export async function initSchedule() {
|
||||
async function saveSemester(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('semester-alert');
|
||||
|
||||
const yearId = semesterYearSelect.value;
|
||||
const payload = {
|
||||
semesterType: semesterTypeSelect.value,
|
||||
startDate: semesterStartInput.value,
|
||||
endDate: semesterEndInput.value
|
||||
};
|
||||
|
||||
try {
|
||||
if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) {
|
||||
throw new Error('Заполните учебный год, тип и даты семестра');
|
||||
}
|
||||
if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) throw new Error('Заполните учебный год, тип и даты семестра');
|
||||
const id = semesterIdInput.value;
|
||||
await (id
|
||||
? api.put('/api/admin/calendar/semesters/' + id, payload)
|
||||
@@ -641,174 +603,268 @@ export async function initSchedule() {
|
||||
hideAlert('semester-alert');
|
||||
}
|
||||
|
||||
async function saveHoliday(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('holiday-alert');
|
||||
async function loadStudyForms() {
|
||||
studyForms = await api.get('/api/admin/academic-calendars/study-forms');
|
||||
populateStudyFormSelect();
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: holidayIdInput.value ? Number(holidayIdInput.value) : null,
|
||||
academicYearId: Number(holidayYearSelect.value),
|
||||
date: holidayDateInput.value,
|
||||
description: holidayDescriptionInput.value.trim()
|
||||
};
|
||||
async function loadActivityTypes() {
|
||||
activityTypes = await api.get('/api/admin/calendar/activity-types');
|
||||
populateActivitySelects();
|
||||
}
|
||||
|
||||
async function loadCalendars() {
|
||||
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
if (!payload.academicYearId || !payload.date) {
|
||||
throw new Error('Выберите учебный год и дату');
|
||||
}
|
||||
const id = holidayIdInput.value;
|
||||
await (id
|
||||
? api.put('/api/admin/calendar/holidays/' + id, payload)
|
||||
: api.post('/api/admin/calendar/holidays', payload));
|
||||
showAlert('holiday-alert', 'Дата сохранена', 'success');
|
||||
resetHolidayForm(false);
|
||||
await loadHolidays();
|
||||
calendars = await api.get('/api/admin/academic-calendars');
|
||||
renderCalendars();
|
||||
populateEditorCalendarSelect();
|
||||
} catch (error) {
|
||||
showAlert('holiday-alert', error.message || 'Ошибка сохранения даты', 'error');
|
||||
calendarsTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHolidays() {
|
||||
const yearId = holidayYearSelect.value;
|
||||
if (!yearId) {
|
||||
holidaysTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Выберите учебный год</td></tr>';
|
||||
function renderCalendars() {
|
||||
if (!calendars.length) {
|
||||
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Календарные графики не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
holidaysTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
holidays = await api.get('/api/admin/calendar/holidays?academicYearId=' + encodeURIComponent(yearId));
|
||||
renderHolidays();
|
||||
} catch (error) {
|
||||
holidaysTbody.innerHTML = `<tr><td colspan="3" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderHolidays() {
|
||||
if (!holidays || !holidays.length) {
|
||||
holidaysTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Исключения не добавлены</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
holidaysTbody.innerHTML = holidays.map(holiday => `
|
||||
calendarsTbody.innerHTML = calendars.map(calendar => `
|
||||
<tr>
|
||||
<td>${escapeHtml(holiday.date)}</td>
|
||||
<td>${escapeHtml(holiday.description || '-')}</td>
|
||||
<td>${escapeHtml(calendar.title)}</td>
|
||||
<td>${escapeHtml(calendar.academicYearTitle || '-')}</td>
|
||||
<td>${escapeHtml(`${calendar.specialtyCode || ''} ${calendar.specialtyName || ''}`.trim() || '-')}</td>
|
||||
<td>${escapeHtml(calendar.specialtyProfileName || '-')}</td>
|
||||
<td>${escapeHtml(calendar.studyFormName || '-')}</td>
|
||||
<td>${escapeHtml(String(calendar.courseCount || '-'))}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-holiday" data-id="${holiday.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-holiday" data-id="${holiday.id}">Удалить</button>
|
||||
<button class="btn-edit-classroom btn-edit-calendar" data-id="${calendar.id}">Изменить</button>
|
||||
<button class="btn-edit-classroom btn-open-calendar-grid" data-id="${calendar.id}">Сетка</button>
|
||||
<button class="btn-delete btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function handleHolidayTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-holiday');
|
||||
const deleteButton = event.target.closest('.btn-delete-holiday');
|
||||
async function saveCalendar(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('academic-calendar-alert');
|
||||
const payload = {
|
||||
id: calendarIdInput.value ? Number(calendarIdInput.value) : null,
|
||||
title: calendarTitleInput.value.trim(),
|
||||
academicYearId: Number(calendarYearSelect.value),
|
||||
specialtyId: Number(calendarSpecialtySelect.value),
|
||||
specialtyProfileId: Number(calendarProfileSelect.value),
|
||||
studyFormId: Number(calendarStudyFormSelect.value),
|
||||
courseCount: Number(calendarCourseCountInput.value)
|
||||
};
|
||||
try {
|
||||
if (!payload.title || !payload.academicYearId || !payload.specialtyId
|
||||
|| !payload.specialtyProfileId || !payload.studyFormId || !payload.courseCount) {
|
||||
throw new Error('Заполните все поля календарного графика');
|
||||
}
|
||||
const id = calendarIdInput.value;
|
||||
const saved = id
|
||||
? await api.put('/api/admin/academic-calendars/' + id, payload)
|
||||
: await api.post('/api/admin/academic-calendars', payload);
|
||||
showAlert('academic-calendar-alert', `График "${escapeHtml(saved.title)}" сохранён`, 'success');
|
||||
resetCalendarForm();
|
||||
await loadCalendars();
|
||||
} catch (error) {
|
||||
showAlert('academic-calendar-alert', error.message || 'Ошибка сохранения календарного графика', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCalendarTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-calendar');
|
||||
const gridButton = event.target.closest('.btn-open-calendar-grid');
|
||||
const deleteButton = event.target.closest('.btn-delete-calendar');
|
||||
if (editButton) {
|
||||
const holiday = holidays.find(item => item.id == editButton.dataset.id);
|
||||
if (!holiday) return;
|
||||
holidayIdInput.value = holiday.id;
|
||||
holidayYearSelect.value = holiday.academicYearId || holidayYearSelect.value;
|
||||
holidayDateInput.value = holiday.date || '';
|
||||
holidayDescriptionInput.value = holiday.description || '';
|
||||
syncSelects(holidayYearSelect);
|
||||
hideAlert('holiday-alert');
|
||||
holidayForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
const calendar = calendars.find(item => item.id == editButton.dataset.id);
|
||||
if (!calendar) return;
|
||||
calendarFormTitle.textContent = 'Редактирование календарного графика';
|
||||
calendarIdInput.value = calendar.id;
|
||||
calendarTitleInput.value = calendar.title || '';
|
||||
calendarYearSelect.value = calendar.academicYearId || '';
|
||||
calendarSpecialtySelect.value = calendar.specialtyId || '';
|
||||
populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value, calendar.specialtyProfileId);
|
||||
calendarStudyFormSelect.value = calendar.studyFormId || '';
|
||||
calendarCourseCountInput.value = calendar.courseCount || 4;
|
||||
syncSelects(calendarYearSelect, calendarSpecialtySelect, calendarProfileSelect, calendarStudyFormSelect);
|
||||
hideAlert('academic-calendar-alert');
|
||||
calendarForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
if (gridButton) {
|
||||
editorCalendarSelect.value = gridButton.dataset.id;
|
||||
syncSelects(editorCalendarSelect);
|
||||
await loadCalendarGrid();
|
||||
calendarGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteButton) {
|
||||
if (!confirm('Удалить дату-исключение?')) return;
|
||||
if (!confirm('Удалить календарный график?')) return;
|
||||
try {
|
||||
await api.delete('/api/admin/calendar/holidays/' + deleteButton.dataset.id);
|
||||
showAlert('holiday-alert', 'Дата удалена', 'success');
|
||||
await loadHolidays();
|
||||
await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
|
||||
showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
|
||||
await loadCalendars();
|
||||
} catch (error) {
|
||||
showAlert('holiday-alert', error.message || 'Ошибка удаления даты', 'error');
|
||||
showAlert('academic-calendar-alert', error.message || 'Ошибка удаления календарного графика', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetHolidayForm(clearYear = true) {
|
||||
const yearId = holidayYearSelect.value;
|
||||
holidayForm.reset();
|
||||
if (!clearYear) holidayYearSelect.value = yearId;
|
||||
holidayIdInput.value = '';
|
||||
syncSelects(holidayYearSelect);
|
||||
hideAlert('holiday-alert');
|
||||
function resetCalendarForm() {
|
||||
calendarForm.reset();
|
||||
calendarFormTitle.textContent = 'Календарный учебный график';
|
||||
calendarIdInput.value = '';
|
||||
calendarCourseCountInput.value = 4;
|
||||
populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value);
|
||||
syncSelects(calendarYearSelect, calendarSpecialtySelect, calendarProfileSelect, calendarStudyFormSelect);
|
||||
hideAlert('academic-calendar-alert');
|
||||
}
|
||||
|
||||
async function loadMatrix() {
|
||||
hideAlert('matrix-alert');
|
||||
const semesterId = matrixSemesterSelect.value;
|
||||
const courseNumber = Number(matrixCourseInput.value);
|
||||
const specialtyId = matrixSpecialtySelect.value;
|
||||
|
||||
if (!semesterId || !courseNumber || !specialtyId) {
|
||||
showAlert('matrix-alert', 'Выберите семестр, курс и специальность', 'error');
|
||||
async function loadCalendarGrid() {
|
||||
hideAlert('calendar-editor-alert');
|
||||
const calendar = selectedEditorCalendar();
|
||||
if (!calendar) {
|
||||
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({ semesterId, courseNumber: String(courseNumber), specialtyId });
|
||||
const rows = await api.get('/api/admin/calendar/matrix?' + params.toString());
|
||||
renderMatrix(rows && rows.length ? rows : buildDefaultMatrixRows(semesterId, courseNumber, specialtyId));
|
||||
showAlert('matrix-alert', rows && rows.length ? 'Матрица загружена' : 'Матрица не найдена, подготовлена новая сетка', 'success');
|
||||
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
|
||||
currentGridRows = rows.length ? rows : buildDefaultGrid(calendar);
|
||||
renderCalendarGrid(currentGridRows);
|
||||
showAlert('calendar-editor-alert', rows.length ? 'Сетка загружена' : 'Подготовлена новая сетка по датам учебного года', 'success');
|
||||
} catch (error) {
|
||||
showAlert('matrix-alert', error.message || 'Ошибка загрузки матрицы', 'error');
|
||||
showAlert('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMatrix() {
|
||||
hideAlert('matrix-alert');
|
||||
const rows = Array.from(matrixGrid.querySelectorAll('.matrix-week')).map(item => ({
|
||||
id: item.dataset.id ? Number(item.dataset.id) : null,
|
||||
semesterId: Number(item.dataset.semesterId),
|
||||
courseNumber: Number(item.dataset.courseNumber),
|
||||
specialtyId: Number(item.dataset.specialtyId),
|
||||
weekNumber: Number(item.dataset.weekNumber),
|
||||
activityType: item.querySelector('select').value
|
||||
async function saveCalendarGrid() {
|
||||
hideAlert('calendar-editor-alert');
|
||||
const calendar = selectedEditorCalendar();
|
||||
if (!calendar) {
|
||||
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
|
||||
return;
|
||||
}
|
||||
const rows = Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => ({
|
||||
calendarId: calendar.id,
|
||||
courseNumber: Number(day.dataset.course),
|
||||
date: day.dataset.date,
|
||||
weekNumber: Number(day.dataset.week),
|
||||
dayOfWeek: Number(day.dataset.day),
|
||||
activityTypeId: Number(day.querySelector('.calendar-day-activity').value)
|
||||
}));
|
||||
|
||||
if (!rows.length) {
|
||||
showAlert('matrix-alert', 'Сначала загрузите матрицу', 'error');
|
||||
showAlert('calendar-editor-alert', 'Сначала загрузите сетку графика', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.put('/api/admin/calendar/matrix', rows);
|
||||
showAlert('matrix-alert', 'Матрица учебного графика сохранена', 'success');
|
||||
await loadMatrix();
|
||||
await api.put(`/api/admin/academic-calendars/${calendar.id}/grid`, rows);
|
||||
showAlert('calendar-editor-alert', 'Календарный учебный график сохранён', 'success');
|
||||
await loadCalendarGrid();
|
||||
} catch (error) {
|
||||
showAlert('matrix-alert', error.message || 'Ошибка сохранения матрицы', 'error');
|
||||
showAlert('calendar-editor-alert', error.message || 'Ошибка сохранения сетки', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderMatrix(rows) {
|
||||
matrixGrid.innerHTML = rows.map(row => `
|
||||
<div class="matrix-week"
|
||||
data-id="${row.id || ''}"
|
||||
data-semester-id="${row.semesterId}"
|
||||
data-course-number="${row.courseNumber}"
|
||||
data-specialty-id="${row.specialtyId}"
|
||||
data-week-number="${row.weekNumber}">
|
||||
<span>${row.weekNumber}</span>
|
||||
<select>${options(activityOptions(), row.activityType || 'THEORY')}</select>
|
||||
</div>
|
||||
`).join('');
|
||||
function applyCalendarFill() {
|
||||
hideAlert('calendar-editor-alert');
|
||||
const activityId = fillActivitySelect.value;
|
||||
const start = fillStartInput.value;
|
||||
const end = fillEndInput.value;
|
||||
if (!activityId || !start || !end) {
|
||||
showAlert('calendar-editor-alert', 'Выберите даты и код активности для заполнения', 'error');
|
||||
return;
|
||||
}
|
||||
if (end < start) {
|
||||
showAlert('calendar-editor-alert', 'Дата окончания не может быть раньше даты начала', 'error');
|
||||
return;
|
||||
}
|
||||
const course = fillCourseSelect.value;
|
||||
let changed = 0;
|
||||
calendarGrid.querySelectorAll('.calendar-day').forEach(day => {
|
||||
const sameCourse = !course || day.dataset.course === course;
|
||||
const inRange = day.dataset.date >= start && day.dataset.date <= end;
|
||||
if (sameCourse && inRange) {
|
||||
day.querySelector('.calendar-day-activity').value = activityId;
|
||||
changed += 1;
|
||||
}
|
||||
});
|
||||
renderCalendarTotals();
|
||||
showAlert('calendar-editor-alert', `Обновлено ячеек: ${changed}`, 'success');
|
||||
}
|
||||
|
||||
function buildDefaultMatrixRows(semesterId, courseNumber, specialtyId) {
|
||||
return Array.from({ length: 22 }, (_, index) => ({
|
||||
id: null,
|
||||
semesterId: Number(semesterId),
|
||||
courseNumber,
|
||||
specialtyId: Number(specialtyId),
|
||||
weekNumber: index + 1,
|
||||
activityType: 'THEORY'
|
||||
}));
|
||||
function renderCalendarGrid(rows) {
|
||||
const grouped = groupBy(rows, row => row.courseNumber);
|
||||
calendarGrid.innerHTML = Array.from(grouped.entries()).map(([course, courseRows]) => `
|
||||
<section class="calendar-course-block">
|
||||
<div class="calendar-course-header">
|
||||
<h3>${course} курс</h3>
|
||||
<span>${courseRows.length} дней</span>
|
||||
</div>
|
||||
<div class="calendar-days-grid">
|
||||
${courseRows.map(renderCalendarDay).join('')}
|
||||
</div>
|
||||
</section>
|
||||
`).join('');
|
||||
populateFillCourseSelect();
|
||||
renderCalendarTotals();
|
||||
}
|
||||
|
||||
function renderCalendarDay(row) {
|
||||
const selected = row.activityTypeId || activityIdByCode(row.activityCode) || theoryActivityId();
|
||||
const dayName = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'][Number(row.dayOfWeek) - 1] || '';
|
||||
return `
|
||||
<div class="calendar-day"
|
||||
data-course="${row.courseNumber}"
|
||||
data-date="${row.date}"
|
||||
data-week="${row.weekNumber}"
|
||||
data-day="${row.dayOfWeek}">
|
||||
<div class="calendar-day-meta">
|
||||
<span>${formatShortDate(row.date)}</span>
|
||||
<span>${dayName}</span>
|
||||
</div>
|
||||
<select class="calendar-day-activity">${options(activityTypes.map(toActivityOption), selected)}</select>
|
||||
<span class="calendar-week-label">${row.weekNumber} нед.</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildDefaultGrid(calendar) {
|
||||
const rows = [];
|
||||
const dates = enumerateDates(calendar.academicYearStartDate, calendar.academicYearEndDate);
|
||||
for (let course = 1; course <= calendar.courseCount; course += 1) {
|
||||
dates.forEach((date, index) => {
|
||||
rows.push({
|
||||
id: null,
|
||||
calendarId: calendar.id,
|
||||
courseNumber: course,
|
||||
date: date.iso,
|
||||
weekNumber: Math.floor(index / 7) + 1,
|
||||
dayOfWeek: date.dayOfWeek,
|
||||
activityTypeId: theoryActivityId(),
|
||||
activityCode: 'Т'
|
||||
});
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderCalendarTotals() {
|
||||
const counts = new Map();
|
||||
calendarGrid.querySelectorAll('.calendar-day-activity').forEach(select => {
|
||||
const activity = activityById(select.value);
|
||||
const key = activity ? activity.code : '-';
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
});
|
||||
calendarTotals.innerHTML = Array.from(counts.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b, 'ru'))
|
||||
.map(([code, count]) => {
|
||||
const activity = activityTypes.find(item => item.code === code);
|
||||
return `<span class="calendar-total-chip" style="--chip-color:${escapeHtml(activity?.colorCode || '#64748b')}">${escapeHtml(code)}: ${count}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function populateSubjectSelects() {
|
||||
@@ -820,26 +876,66 @@ export async function initSchedule() {
|
||||
const yearOptions = '<option value="">Выберите учебный год</option>' +
|
||||
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
||||
semesterYearSelect.innerHTML = yearOptions;
|
||||
holidayYearSelect.innerHTML = yearOptions;
|
||||
|
||||
if (!holidayYearSelect.value && academicYears.length) {
|
||||
holidayYearSelect.value = String(academicYears[0].id);
|
||||
}
|
||||
syncSelects(semesterYearSelect, holidayYearSelect);
|
||||
calendarYearSelect.innerHTML = yearOptions;
|
||||
syncSelects(semesterYearSelect, calendarYearSelect);
|
||||
}
|
||||
|
||||
function populateSemesterSelects() {
|
||||
const semesterOptions = '<option value="">Выберите семестр</option>' +
|
||||
semesters.map(semester => `<option value="${semester.id}">${escapeHtml(semesterLabel(semester))}</option>`).join('');
|
||||
ruleSemesterSelect.innerHTML = semesterOptions;
|
||||
matrixSemesterSelect.innerHTML = semesterOptions;
|
||||
syncSelects(ruleSemesterSelect, matrixSemesterSelect);
|
||||
syncSelects(ruleSemesterSelect);
|
||||
}
|
||||
|
||||
function populateSpecialtySelects() {
|
||||
matrixSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||
calendarSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||
specialties.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality))}</option>`).join('');
|
||||
syncSelects(matrixSpecialtySelect);
|
||||
populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value);
|
||||
syncSelects(calendarSpecialtySelect);
|
||||
}
|
||||
|
||||
function populateProfileSelect(select, specialtyId, selectedProfileId = null) {
|
||||
const profiles = profilesBySpecialty.get(String(specialtyId)) || [];
|
||||
select.innerHTML = profiles.length
|
||||
? '<option value="">Выберите профиль</option>' + profiles.map(profile =>
|
||||
`<option value="${profile.id}">${escapeHtml(profile.name)}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет профилей</option>';
|
||||
if (selectedProfileId && profiles.some(profile => String(profile.id) === String(selectedProfileId))) {
|
||||
select.value = selectedProfileId;
|
||||
}
|
||||
syncSelects(select);
|
||||
}
|
||||
|
||||
function populateStudyFormSelect() {
|
||||
calendarStudyFormSelect.innerHTML = '<option value="">Выберите форму</option>' +
|
||||
studyForms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
|
||||
syncSelects(calendarStudyFormSelect);
|
||||
}
|
||||
|
||||
function populateActivitySelects() {
|
||||
const activityOptions = '<option value="">Выберите код</option>' +
|
||||
activityTypes.map(type => `<option value="${type.id}">${escapeHtml(type.code)} - ${escapeHtml(type.name)}</option>`).join('');
|
||||
fillActivitySelect.innerHTML = activityOptions;
|
||||
syncSelects(fillActivitySelect);
|
||||
}
|
||||
|
||||
function populateEditorCalendarSelect() {
|
||||
const current = editorCalendarSelect.value;
|
||||
editorCalendarSelect.innerHTML = '<option value="">Выберите график</option>' +
|
||||
calendars.map(calendar => `<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`).join('');
|
||||
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
|
||||
editorCalendarSelect.value = current;
|
||||
}
|
||||
syncSelects(editorCalendarSelect);
|
||||
}
|
||||
|
||||
function populateFillCourseSelect() {
|
||||
const courses = new Set(Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => day.dataset.course));
|
||||
fillCourseSelect.innerHTML = '<option value="">Все курсы</option>' +
|
||||
Array.from(courses).sort((a, b) => Number(a) - Number(b))
|
||||
.map(course => `<option value="${course}">${course} курс</option>`).join('');
|
||||
syncSelects(fillCourseSelect);
|
||||
}
|
||||
|
||||
function options(items, selectedValue) {
|
||||
@@ -850,8 +946,8 @@ export async function initSchedule() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function activityOptions() {
|
||||
return Object.entries(ACTIVITY_LABELS).map(([value, label]) => ({ value, label }));
|
||||
function toActivityOption(type) {
|
||||
return { value: type.id, label: `${type.code} - ${type.name}` };
|
||||
}
|
||||
|
||||
function toTimeSlotOption(slot) {
|
||||
@@ -871,9 +967,25 @@ export async function initSchedule() {
|
||||
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
|
||||
}
|
||||
|
||||
function selectedEditorCalendar() {
|
||||
return calendars.find(calendar => String(calendar.id) === String(editorCalendarSelect.value));
|
||||
}
|
||||
|
||||
function theoryActivityId() {
|
||||
return activityTypes.find(type => type.code === 'Т')?.id || activityTypes[0]?.id || '';
|
||||
}
|
||||
|
||||
function activityIdByCode(code) {
|
||||
return activityTypes.find(type => type.code === code)?.id || null;
|
||||
}
|
||||
|
||||
function activityById(id) {
|
||||
return activityTypes.find(type => String(type.id) === String(id));
|
||||
}
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} — ${semester.endDate})`;
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;
|
||||
}
|
||||
|
||||
function formatSemester(rule) {
|
||||
@@ -888,7 +1000,7 @@ export async function initSchedule() {
|
||||
function specialityLabel(speciality) {
|
||||
const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id;
|
||||
const name = speciality.specialityName || speciality.name || '';
|
||||
return name ? `${code} — ${name}` : String(code);
|
||||
return name ? `${code} - ${name}` : String(code);
|
||||
}
|
||||
|
||||
function trimTime(value) {
|
||||
@@ -896,6 +1008,34 @@ export async function initSchedule() {
|
||||
return String(value).slice(0, 5);
|
||||
}
|
||||
|
||||
function formatShortDate(value) {
|
||||
const [year, month, day] = String(value).split('-');
|
||||
return `${day}.${month}`;
|
||||
}
|
||||
|
||||
function enumerateDates(startIso, endIso) {
|
||||
const result = [];
|
||||
const current = new Date(`${startIso}T00:00:00Z`);
|
||||
const end = new Date(`${endIso}T00:00:00Z`);
|
||||
while (current <= end) {
|
||||
const iso = current.toISOString().slice(0, 10);
|
||||
const jsDay = current.getUTCDay();
|
||||
result.push({ iso, dayOfWeek: jsDay === 0 ? 7 : jsDay });
|
||||
current.setUTCDate(current.getUTCDate() + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function groupBy(items, keyFn) {
|
||||
const grouped = new Map();
|
||||
items.forEach(item => {
|
||||
const key = keyFn(item);
|
||||
if (!grouped.has(key)) grouped.set(key, []);
|
||||
grouped.get(key).push(item);
|
||||
});
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function syncSelects(...selects) {
|
||||
selects.filter(Boolean).forEach(select => {
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
@@ -55,20 +55,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Container -->
|
||||
<!-- Контейнер таблицы -->
|
||||
<div class="workload-grid-container">
|
||||
<table class="workload-table" id="workload-table">
|
||||
<thead>
|
||||
<tr id="workload-header-row">
|
||||
<th class="top-left-cell">
|
||||
<span class="top-label">Аудитория</span>
|
||||
<span class="bottom-label">Время</span>
|
||||
<span class="top-label">Время</span>
|
||||
<span class="bottom-label">Аудитория</span>
|
||||
</th>
|
||||
<!-- Rendered by JS -->
|
||||
<!-- Заполняется через JS -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="workload-tbody">
|
||||
<!-- Rendered by JS -->
|
||||
<!-- Заполняется через JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Профили обучения</h2>
|
||||
<form id="create-profile-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="profile-specialty">Специальность</label>
|
||||
<select id="profile-specialty" required>
|
||||
<option value="">Сначала создайте специальность</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile-name">Название профиля</label>
|
||||
<input type="text" id="profile-name" placeholder="Например: Безопасность автоматизированных систем" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile-description">Описание</label>
|
||||
<input type="text" id="profile-description" placeholder="Необязательно">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать профиль</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-profile-alert" role="alert"></div>
|
||||
</form>
|
||||
<div class="table-wrap schedule-inline-table">
|
||||
<table id="profiles-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Профиль</th>
|
||||
<th>Описание</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="profiles-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Выберите специальность</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Создание специальности</h2>
|
||||
<form id="create-specialty-form">
|
||||
@@ -124,3 +165,25 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="modal-edit-profile">
|
||||
<div class="modal-content card">
|
||||
<h2>Редактирование профиля</h2>
|
||||
<button class="modal-close" id="modal-edit-profile-close">×</button>
|
||||
<form id="edit-profile-form">
|
||||
<input type="hidden" id="edit-profile-id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="edit-profile-name">Название профиля</label>
|
||||
<input type="text" id="edit-profile-name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-profile-description">Описание</label>
|
||||
<input type="text" id="edit-profile-description">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-profile-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- ===== Groups Tab ===== -->
|
||||
<!-- ===== Вкладка групп ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая группа</h2>
|
||||
<form id="create-group-form">
|
||||
@@ -33,6 +33,12 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-profile">Профиль обучения</label>
|
||||
<select id="new-group-profile">
|
||||
<option value="">Выберите специальность</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-group-alert" role="alert"></div>
|
||||
@@ -60,14 +66,114 @@
|
||||
<th>Кафедра</th>
|
||||
<th>Курс</th>
|
||||
<th>Специальность</th>
|
||||
<th>Профиль</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="groups-tbody">
|
||||
<tr>
|
||||
<td colspan="8" class="loading-row">Загрузка...</td>
|
||||
<td colspan="9" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Назначение календарного графика</h2>
|
||||
<form id="group-calendar-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="calendar-group">Группа</label>
|
||||
<select id="calendar-group">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-year">Учебный год</label>
|
||||
<select id="calendar-year">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-id">Календарный график</label>
|
||||
<select id="calendar-id">
|
||||
<option value="">Выберите группу и год</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Назначить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="group-calendar-alert" role="alert"></div>
|
||||
</form>
|
||||
<div class="table-wrap schedule-inline-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Учебный год</th>
|
||||
<th>График</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="group-calendar-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Выберите группу</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно редактирования группы -->
|
||||
<div class="modal-overlay" id="modal-edit-group">
|
||||
<div class="modal-content card">
|
||||
<h2>Редактировать группу</h2>
|
||||
<button class="modal-close" id="modal-edit-group-close">×</button>
|
||||
<form id="edit-group-form">
|
||||
<input type="hidden" id="edit-group-id">
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="edit-group-name">Название группы</label>
|
||||
<input type="text" id="edit-group-name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-size">Численность группы</label>
|
||||
<input type="number" id="edit-group-size" min="1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-ef">Форма обучения</label>
|
||||
<select id="edit-group-ef">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="edit-group-department">Кафедра</label>
|
||||
<select id="edit-group-department">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-yearStartStudy">Год начала обучения</label>
|
||||
<input type="number" id="edit-group-yearStartStudy" min="2000" max="2100" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-speciality-code">Специальность</label>
|
||||
<select id="edit-group-speciality-code">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-group-profile">Профиль обучения</label>
|
||||
<select id="edit-group-profile">
|
||||
<option value="">Выберите специальность</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" style="width: 100%;">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-group-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -213,41 +213,72 @@
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2 id="holiday-form-title">Праздники и исключения</h2>
|
||||
<form id="holiday-form" novalidate>
|
||||
<input type="hidden" id="holiday-id">
|
||||
<div class="card-header-row">
|
||||
<h2 id="academic-calendar-form-title">Календарный учебный график</h2>
|
||||
<button type="button" class="btn-edit-classroom" id="academic-calendar-reset">Очистить</button>
|
||||
</div>
|
||||
<form id="academic-calendar-form" novalidate>
|
||||
<input type="hidden" id="academic-calendar-id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="holiday-year">Учебный год</label>
|
||||
<select id="holiday-year" required>
|
||||
<label for="academic-calendar-title">Название</label>
|
||||
<input type="text" id="academic-calendar-title" placeholder="09.03.04 очная форма 2025-2026" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="academic-calendar-year">Учебный год</label>
|
||||
<select id="academic-calendar-year" required>
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="holiday-date">Дата</label>
|
||||
<input type="date" id="holiday-date" required>
|
||||
<label for="academic-calendar-specialty">Специальность</label>
|
||||
<select id="academic-calendar-specialty" required>
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="holiday-description">Описание</label>
|
||||
<input type="text" id="holiday-description" placeholder="Праздничный день">
|
||||
<label for="academic-calendar-profile">Профиль</label>
|
||||
<select id="academic-calendar-profile" required>
|
||||
<option value="">Выберите специальность</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить дату</button>
|
||||
<button type="button" class="btn-edit-classroom" id="holiday-reset">Очистить</button>
|
||||
<div class="form-group">
|
||||
<label for="academic-calendar-study-form">Форма обучения</label>
|
||||
<select id="academic-calendar-study-form" required>
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="academic-calendar-course-count">Курсов</label>
|
||||
<input type="number" id="academic-calendar-course-count" min="1" max="8" value="4" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Сохранить график</button>
|
||||
</div>
|
||||
<div class="form-alert" id="holiday-alert" role="alert"></div>
|
||||
<div class="form-alert" id="academic-calendar-alert" role="alert"></div>
|
||||
</form>
|
||||
<div class="table-wrap schedule-inline-table">
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Календарные графики</h2>
|
||||
<button class="btn-primary" id="academic-calendars-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Дата</th>
|
||||
<th>Описание</th>
|
||||
<th>Название</th>
|
||||
<th>Учебный год</th>
|
||||
<th>Специальность</th>
|
||||
<th>Профиль</th>
|
||||
<th>Форма</th>
|
||||
<th>Курсов</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="holidays-tbody">
|
||||
<tbody id="academic-calendars-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Выберите учебный год</td>
|
||||
<td colspan="7" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -255,27 +286,41 @@
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Матрица учебного графика</h2>
|
||||
<h2>Редактор годового графика</h2>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="matrix-semester">Семестр</label>
|
||||
<select id="matrix-semester">
|
||||
<label for="calendar-editor-calendar">График</label>
|
||||
<select id="calendar-editor-calendar">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="matrix-course">Курс</label>
|
||||
<input type="number" id="matrix-course" min="1" step="1" placeholder="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="matrix-specialty">Специальность</label>
|
||||
<select id="matrix-specialty">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="matrix-load">Загрузить</button>
|
||||
<button type="button" class="btn-primary" id="matrix-save">Сохранить</button>
|
||||
<button type="button" class="btn-primary" id="calendar-editor-load">Загрузить сетку</button>
|
||||
<button type="button" class="btn-primary" id="calendar-editor-save">Сохранить сетку</button>
|
||||
</div>
|
||||
<div class="form-alert" id="matrix-alert" role="alert"></div>
|
||||
<div id="matrix-grid" class="matrix-grid"></div>
|
||||
<div class="calendar-fill-panel">
|
||||
<div class="form-group">
|
||||
<label for="calendar-fill-course">Курс</label>
|
||||
<select id="calendar-fill-course">
|
||||
<option value="">Все курсы</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-fill-start">С даты</label>
|
||||
<input type="date" id="calendar-fill-start">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-fill-end">По дату</label>
|
||||
<input type="date" id="calendar-fill-end">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-fill-activity">Код</label>
|
||||
<select id="calendar-fill-activity">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-edit-classroom" id="calendar-fill-apply">Заполнить диапазон</button>
|
||||
</div>
|
||||
<div class="form-alert" id="calendar-editor-alert" role="alert"></div>
|
||||
<div id="calendar-totals" class="calendar-totals"></div>
|
||||
<div id="calendar-grid" class="academic-calendar-grid"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user