добавил денение на подгруппы для лабораторных работ
This commit is contained in:
@@ -164,7 +164,7 @@
|
||||
|
||||
.schedule-slot-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(130px, 1fr)) auto;
|
||||
grid-template-columns: repeat(8, minmax(125px, 1fr)) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
padding: 0.75rem;
|
||||
|
||||
@@ -22,8 +22,17 @@ export async function initGroups() {
|
||||
const calendarYearSelect = document.getElementById('calendar-year');
|
||||
const calendarSelect = document.getElementById('calendar-id');
|
||||
const calendarAssignmentsTbody = document.getElementById('group-calendar-tbody');
|
||||
const subgroupForm = document.getElementById('subgroup-form');
|
||||
const subgroupIdInput = document.getElementById('subgroup-id');
|
||||
const subgroupGroupSelect = document.getElementById('subgroup-group');
|
||||
const subgroupNameInput = document.getElementById('subgroup-name');
|
||||
const subgroupCapacityInput = document.getElementById('subgroup-capacity');
|
||||
const subgroupSubmitButton = document.getElementById('subgroup-submit');
|
||||
const subgroupResetButton = document.getElementById('subgroup-reset');
|
||||
const subgroupsTbody = document.getElementById('subgroups-tbody');
|
||||
|
||||
let allGroups = [];
|
||||
let subgroups = [];
|
||||
let educationForms = [];
|
||||
let departments = [];
|
||||
let specialties = [];
|
||||
@@ -47,7 +56,11 @@ export async function initGroups() {
|
||||
createGroupForm.addEventListener('submit', createGroup);
|
||||
editGroupForm.addEventListener('submit', updateGroup);
|
||||
calendarForm.addEventListener('submit', saveAssignment);
|
||||
subgroupForm.addEventListener('submit', saveSubgroup);
|
||||
subgroupGroupSelect.addEventListener('change', renderSubgroups);
|
||||
subgroupResetButton.addEventListener('click', resetSubgroupForm);
|
||||
groupsTbody.addEventListener('click', handleGroupTableClick);
|
||||
subgroupsTbody.addEventListener('click', handleSubgroupTableClick);
|
||||
calendarAssignmentsTbody.addEventListener('click', handleAssignmentTableClick);
|
||||
modalEditGroupClose.addEventListener('click', () => modalEditGroup.classList.remove('open'));
|
||||
modalEditGroup.addEventListener('click', (event) => {
|
||||
@@ -73,8 +86,10 @@ export async function initGroups() {
|
||||
populateYearSelect();
|
||||
populateCalendarSelect();
|
||||
await loadGroups();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки данных: ${escapeHtml(error.message)}</td></tr>`;
|
||||
subgroupsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки данных: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +106,22 @@ export async function initGroups() {
|
||||
allGroups = await api.get('/api/groups');
|
||||
applyGroupFilter();
|
||||
populateGroupSelect();
|
||||
populateSubgroupGroupSelect();
|
||||
await loadAssignments();
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubgroups() {
|
||||
try {
|
||||
subgroups = await api.get('/api/subgroups');
|
||||
renderSubgroups();
|
||||
} catch (error) {
|
||||
subgroupsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyGroupFilter() {
|
||||
const filterId = filterEfSelect.value;
|
||||
const filtered = filterId
|
||||
@@ -217,6 +242,18 @@ export async function initGroups() {
|
||||
syncSelects(calendarGroupSelect);
|
||||
}
|
||||
|
||||
function populateSubgroupGroupSelect() {
|
||||
const current = subgroupGroupSelect.value;
|
||||
subgroupGroupSelect.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))) {
|
||||
subgroupGroupSelect.value = current;
|
||||
} else if (allGroups.length) {
|
||||
subgroupGroupSelect.value = String(allGroups[0].id);
|
||||
}
|
||||
syncSelects(subgroupGroupSelect);
|
||||
}
|
||||
|
||||
function populateCalendarSelect() {
|
||||
const group = selectedCalendarGroup();
|
||||
const yearId = calendarYearSelect.value;
|
||||
@@ -248,6 +285,7 @@ export async function initGroups() {
|
||||
createGroupForm.reset();
|
||||
syncSelects(newGroupEfSelect, newGroupDepartmentSelect, newGroupSpecialitySelect, newGroupProfileSelect);
|
||||
await loadGroups();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
showAlert('create-group-alert', error.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
@@ -265,6 +303,7 @@ export async function initGroups() {
|
||||
modalEditGroup.classList.remove('open');
|
||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || payload.name)}" обновлена`, 'success');
|
||||
await loadGroups();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
showAlert('edit-group-alert', error.message || 'Ошибка обновления', 'error');
|
||||
}
|
||||
@@ -309,6 +348,7 @@ export async function initGroups() {
|
||||
try {
|
||||
await api.delete('/api/groups/' + btnDelete.dataset.id);
|
||||
await loadGroups();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка удаления');
|
||||
}
|
||||
@@ -339,6 +379,100 @@ export async function initGroups() {
|
||||
modalEditGroup.classList.add('open');
|
||||
}
|
||||
|
||||
async function saveSubgroup(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('subgroup-alert');
|
||||
const groupId = subgroupGroupSelect.value;
|
||||
const name = subgroupNameInput.value.trim();
|
||||
const capacity = subgroupCapacityInput.value;
|
||||
const subgroupId = subgroupIdInput.value;
|
||||
|
||||
if (!groupId) { showAlert('subgroup-alert', 'Выберите группу', 'error'); return; }
|
||||
if (!name) { showAlert('subgroup-alert', 'Введите название подгруппы', 'error'); return; }
|
||||
if (capacity && Number(capacity) < 0) { showAlert('subgroup-alert', 'Численность не может быть отрицательной', 'error'); return; }
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
studentCapacity: capacity ? Number(capacity) : null
|
||||
};
|
||||
|
||||
try {
|
||||
const endpoint = `/api/groups/${groupId}/subgroups${subgroupId ? `/${subgroupId}` : ''}`;
|
||||
const saved = subgroupId
|
||||
? await api.put(endpoint, payload)
|
||||
: await api.post(endpoint, payload);
|
||||
showAlert('subgroup-alert', `Подгруппа "${escapeHtml(saved.name)}" сохранена`, 'success');
|
||||
resetSubgroupForm();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
showAlert('subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubgroups() {
|
||||
const groupId = subgroupGroupSelect.value;
|
||||
const visibleSubgroups = groupId
|
||||
? subgroups.filter(subgroup => String(subgroup.groupId) === String(groupId))
|
||||
: subgroups;
|
||||
if (!visibleSubgroups.length) {
|
||||
subgroupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Подгруппы не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
subgroupsTbody.innerHTML = visibleSubgroups.map(subgroup => `
|
||||
<tr>
|
||||
<td>${escapeHtml(subgroup.groupName || groupName(subgroup.groupId))}</td>
|
||||
<td>${escapeHtml(subgroup.name)}</td>
|
||||
<td>${escapeHtml(String(subgroup.studentCapacity ?? '-'))}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-subgroup" data-id="${subgroup.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-subgroup" data-id="${subgroup.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function handleSubgroupTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-subgroup');
|
||||
const deleteButton = event.target.closest('.btn-delete-subgroup');
|
||||
|
||||
if (editButton) {
|
||||
const subgroup = subgroups.find(item => String(item.id) === String(editButton.dataset.id));
|
||||
if (!subgroup) return;
|
||||
subgroupIdInput.value = subgroup.id;
|
||||
subgroupGroupSelect.value = subgroup.groupId;
|
||||
subgroupGroupSelect.disabled = true;
|
||||
subgroupNameInput.value = subgroup.name || '';
|
||||
subgroupCapacityInput.value = subgroup.studentCapacity ?? '';
|
||||
subgroupSubmitButton.textContent = 'Сохранить подгруппу';
|
||||
syncSelects(subgroupGroupSelect);
|
||||
hideAlert('subgroup-alert');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteButton) {
|
||||
const subgroup = subgroups.find(item => String(item.id) === String(deleteButton.dataset.id));
|
||||
if (!subgroup || !confirm('Удалить подгруппу?')) return;
|
||||
try {
|
||||
await api.delete(`/api/groups/${subgroup.groupId}/subgroups/${subgroup.id}`);
|
||||
showAlert('subgroup-alert', 'Подгруппа удалена', 'success');
|
||||
resetSubgroupForm();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
showAlert('subgroup-alert', error.message || 'Ошибка удаления подгруппы', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetSubgroupForm() {
|
||||
subgroupIdInput.value = '';
|
||||
subgroupNameInput.value = '';
|
||||
subgroupCapacityInput.value = '';
|
||||
subgroupGroupSelect.disabled = false;
|
||||
subgroupSubmitButton.textContent = 'Создать подгруппу';
|
||||
syncSelects(subgroupGroupSelect);
|
||||
hideAlert('subgroup-alert');
|
||||
}
|
||||
|
||||
async function saveAssignment(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('group-calendar-alert');
|
||||
@@ -419,6 +553,11 @@ export async function initGroups() {
|
||||
return department.departmentName || department.name || departmentId;
|
||||
}
|
||||
|
||||
function groupName(groupId) {
|
||||
const group = allGroups.find(item => String(item.id) === String(groupId));
|
||||
return group ? group.name : groupId || '-';
|
||||
}
|
||||
|
||||
function specialityLabel(specialityId) {
|
||||
const speciality = specialties.find(item => item.id == specialityId);
|
||||
if (!speciality) return specialityId || '-';
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function initSchedule() {
|
||||
let teachers = [];
|
||||
let classrooms = [];
|
||||
let lessonTypes = [];
|
||||
let subgroups = [];
|
||||
let timeSlots = [];
|
||||
let semesters = [];
|
||||
|
||||
@@ -99,17 +100,24 @@ export async function initSchedule() {
|
||||
renderRuleSlots(slots.length ? slots : [{}]);
|
||||
});
|
||||
|
||||
ruleGroupsContainer.addEventListener('change', () => renderRuleSlots(readRuleSlots(false)));
|
||||
ruleSlotsContainer.addEventListener('change', (event) => {
|
||||
if (event.target.closest('.slot-lesson-type')) {
|
||||
renderRuleSlots(readRuleSlots(false));
|
||||
}
|
||||
});
|
||||
ruleForm.addEventListener('submit', saveRule);
|
||||
rulesTbody.addEventListener('click', handleRuleTableClick);
|
||||
}
|
||||
|
||||
async function loadBaseLists() {
|
||||
[subjects, groups, teachers, classrooms, lessonTypes] = await Promise.all([
|
||||
[subjects, groups, teachers, classrooms, lessonTypes, subgroups] = await Promise.all([
|
||||
api.get('/api/subjects'),
|
||||
api.get('/api/groups'),
|
||||
api.get('/api/users/teachers'),
|
||||
api.get('/api/classrooms'),
|
||||
api.get('/api/lesson-types')
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/subgroups')
|
||||
]);
|
||||
populateSubjectSelects();
|
||||
}
|
||||
@@ -159,6 +167,7 @@ export async function initSchedule() {
|
||||
slot.dayName,
|
||||
slot.timeSlotLabel,
|
||||
parity,
|
||||
slot.subgroupName,
|
||||
slot.teacherName,
|
||||
slot.classroomName,
|
||||
slot.lessonTypeName,
|
||||
@@ -290,6 +299,7 @@ export async function initSchedule() {
|
||||
<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-subgroup" ${isLaboratorySlot(slot) ? '' : 'disabled'}>${subgroupOptions(slot)}</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>
|
||||
@@ -299,12 +309,13 @@ export async function initSchedule() {
|
||||
function readRuleSlots(validate) {
|
||||
const rows = Array.from(ruleSlotsContainer.querySelectorAll('.schedule-slot-row'));
|
||||
return rows.map((row, index) => {
|
||||
const subgroupValue = row.querySelector('.slot-subgroup')?.value || '';
|
||||
const slot = {
|
||||
id: null,
|
||||
dayOfWeek: Number(row.querySelector('.slot-day').value),
|
||||
parity: row.querySelector('.slot-parity').value,
|
||||
timeSlotId: Number(row.querySelector('.slot-time').value),
|
||||
subgroupId: null,
|
||||
subgroupId: subgroupValue ? Number(subgroupValue) : null,
|
||||
teacherId: Number(row.querySelector('.slot-teacher').value),
|
||||
classroomId: Number(row.querySelector('.slot-classroom').value),
|
||||
lessonTypeId: Number(row.querySelector('.slot-lesson-type').value),
|
||||
@@ -314,6 +325,9 @@ export async function initSchedule() {
|
||||
|| !slot.teacherId || !slot.classroomId || !slot.lessonTypeId || !slot.lessonFormat)) {
|
||||
throw new Error(`Заполните слот ${index + 1}`);
|
||||
}
|
||||
if (validate) {
|
||||
validateSlotSubgroup(slot, index);
|
||||
}
|
||||
return slot;
|
||||
});
|
||||
}
|
||||
@@ -354,6 +368,23 @@ export async function initSchedule() {
|
||||
if (!hasHours) throw new Error('Укажите часы хотя бы для одного типа занятий');
|
||||
}
|
||||
|
||||
function validateSlotSubgroup(slot, index) {
|
||||
const typeKey = lessonTypeKeyById(slot.lessonTypeId);
|
||||
if (slot.subgroupId && typeKey !== 'laboratory') {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа доступна только для лабораторных`);
|
||||
}
|
||||
if (!slot.subgroupId) return;
|
||||
|
||||
const selectedSubgroup = subgroups.find(item => String(item.id) === String(slot.subgroupId));
|
||||
if (!selectedSubgroup) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа не найдена`);
|
||||
}
|
||||
const selectedGroups = new Set(selectedRuleGroupIds().map(String));
|
||||
if (!selectedGroups.has(String(selectedSubgroup.groupId))) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа должна относиться к выбранным группам правила`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTimeSlots() {
|
||||
const slots = await api.get('/api/admin/time-slots');
|
||||
timeSlots = slots
|
||||
@@ -382,6 +413,26 @@ export async function initSchedule() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function subgroupOptions(slot) {
|
||||
if (!isLaboratorySlot(slot)) {
|
||||
return '<option value="">Только для лабораторных</option>';
|
||||
}
|
||||
const selected = slot.subgroupId == null ? '' : String(slot.subgroupId);
|
||||
const selectedGroups = new Set(selectedRuleGroupIds().map(String));
|
||||
const matchingSubgroups = subgroups
|
||||
.filter(subgroup => selectedGroups.has(String(subgroup.groupId)))
|
||||
.sort((a, b) => subgroupLabel(a).localeCompare(subgroupLabel(b), 'ru-RU'));
|
||||
return '<option value="">Вся группа</option>' + matchingSubgroups.map(subgroup => {
|
||||
const value = String(subgroup.id);
|
||||
return `<option value="${escapeHtml(value)}" ${value === selected ? 'selected' : ''}>${escapeHtml(subgroupLabel(subgroup))}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function selectedRuleGroupIds() {
|
||||
return Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(input => Number(input.value));
|
||||
}
|
||||
|
||||
function toTimeSlotOption(slot) {
|
||||
return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` };
|
||||
}
|
||||
@@ -404,6 +455,14 @@ export async function initSchedule() {
|
||||
return lessonType ? lessonTypeKey(lessonType) : null;
|
||||
}
|
||||
|
||||
function isLaboratorySlot(slot) {
|
||||
return lessonTypeKeyById(slot.lessonTypeId) === 'laboratory';
|
||||
}
|
||||
|
||||
function subgroupLabel(subgroup) {
|
||||
return `${subgroup.groupName || `Группа ${subgroup.groupId}`}: ${subgroup.name}`;
|
||||
}
|
||||
|
||||
function lessonTypeKey(lessonType) {
|
||||
const name = String(lessonType.name || lessonType.lessonType || '').toLowerCase();
|
||||
if (name.includes('лекц')) return 'lecture';
|
||||
|
||||
@@ -79,6 +79,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Подгруппы для лабораторных</h2>
|
||||
<form id="subgroup-form">
|
||||
<input type="hidden" id="subgroup-id">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="subgroup-group">Группа</label>
|
||||
<select id="subgroup-group">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subgroup-name">Название подгруппы</label>
|
||||
<input type="text" id="subgroup-name" placeholder="Подгруппа 1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subgroup-capacity">Численность</label>
|
||||
<input type="number" id="subgroup-capacity" min="0" placeholder="12">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" id="subgroup-submit">Создать подгруппу</button>
|
||||
<button type="button" class="btn-edit-classroom" id="subgroup-reset">Очистить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="subgroup-alert" role="alert"></div>
|
||||
</form>
|
||||
<div class="table-wrap schedule-inline-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Группа</th>
|
||||
<th>Подгруппа</th>
|
||||
<th>Численность</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subgroups-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Назначение календарного графика</h2>
|
||||
<form id="group-calendar-form">
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
<div class="lesson-title">${escapeHtml(lesson.subjectName)}</div>
|
||||
<div class="lesson-meta">
|
||||
<span class="chip">${escapeHtml(lesson.lessonTypeName)}</span>
|
||||
${lesson.subgroupName ? `<span class="chip">${escapeHtml(lesson.subgroupName)}</span>` : ''}
|
||||
<span class="chip">${escapeHtml(lesson.lessonFormat)}</span>
|
||||
<span class="chip">${escapeHtml(lesson.teacherName)}</span>
|
||||
<span class="chip">${escapeHtml(lesson.classroomName)}</span>
|
||||
|
||||
@@ -437,6 +437,7 @@
|
||||
<div class="lesson-meta">
|
||||
<span class="chip">${escapeHtml(trimTime(lesson.startTime))} - ${escapeHtml(trimTime(lesson.endTime))}</span>
|
||||
<span class="chip">${escapeHtml(lesson.lessonTypeName)}</span>
|
||||
${lesson.subgroupName ? `<span class="chip">${escapeHtml(lesson.subgroupName)}</span>` : ''}
|
||||
<span class="chip">${escapeHtml(lesson.classroomName)}</span>
|
||||
${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user