Изменил страницу "Кафедра", добавлены изменения из задачи #54 в Vikunja

This commit is contained in:
2026-04-01 23:18:05 +03:00
parent 3cdb8614cb
commit c82e3feaed
4 changed files with 111 additions and 50 deletions

View File

@@ -1,6 +1,9 @@
import { api } from '../api.js'; import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js';
// Ключ для хранения данных в sessionStorage
const STORAGE_KEY = 'department_schedule_blocks';
export async function initDepartment() { export async function initDepartment() {
const form = document.getElementById('department-schedule-form'); const form = document.getElementById('department-schedule-form');
const departmentSelect = document.getElementById('filter-department'); const departmentSelect = document.getElementById('filter-department');
@@ -17,6 +20,9 @@ export async function initDepartment() {
departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>'; departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
} }
// ===== Восстанавливаем ранее загруженные таблицы из sessionStorage =====
restoreScheduleBlocks();
form.addEventListener('submit', async (e) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
hideAlert('schedule-form-alert'); hideAlert('schedule-form-alert');
@@ -39,17 +45,32 @@ export async function initDepartment() {
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType); const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
const periodName = period.replace('-', '/'); const periodName = period.replace('-', '/');
renderScheduleBlock(deptName, semesterName, periodName, data); renderScheduleBlock(deptName, semesterName, periodName, data, departmentId, semesterType, period);
form.reset();
// НЕ сбрасываем форму — фильтры остаются заполненными (fix #3)
} catch (err) { } catch (err) {
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error'); showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
} }
}); });
function renderScheduleBlock(deptName, groupSemester, period, schedule) { // ===== Уникальный ключ для таблицы по параметрам =====
function blockKey(departmentId, semesterType, period) {
return `${departmentId}_${semesterType}_${period}`;
}
// ===== Рендер блока таблицы (с дедупликацией — fix #6) =====
function renderScheduleBlock(deptName, semester, period, schedule, departmentId, semesterType, rawPeriod) {
const key = blockKey(departmentId, semesterType, rawPeriod);
// Удаляем ранее загруженный блок с тем же ключом
const existing = container.querySelector(`[data-block-key="${key}"]`);
if (existing) existing.remove();
const details = document.createElement('details'); const details = document.createElement('details');
details.className = 'table-item'; details.className = 'table-item';
details.open = true; details.open = true;
details.setAttribute('data-block-key', key);
details.innerHTML = ` details.innerHTML = `
<summary> <summary>
<div class="chev" aria-hidden="true"> <div class="chev" aria-hidden="true">
@@ -61,10 +82,10 @@ export async function initDepartment() {
<div class="title title-multiline"> <div class="title title-multiline">
<span class="title-main">Данные к составлению расписания</span> <span class="title-main">Данные к составлению расписания</span>
<span class="title-sub">Кафедра: <b>${escapeHtml(deptName)}</b></span> <span class="title-sub">Кафедра: <b>${escapeHtml(deptName)}</b></span>
<span class="title-sub">Семестр: <b>${escapeHtml(groupSemester)}</b></span> <span class="title-sub">Семестр: <b>${escapeHtml(semester)}</b></span>
<span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span> <span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span>
</div> </div>
<div class="meta">${schedule ? schedule.length : 0} записей</div> <div class="meta">${Array.isArray(schedule) ? schedule.length : 0} записей</div>
</summary> </summary>
<div class="content"> <div class="content">
<table> <table>
@@ -86,11 +107,15 @@ export async function initDepartment() {
</table> </table>
</div> </div>
`; `;
container.prepend(details); container.prepend(details);
// Сохраняем в sessionStorage
saveScheduleBlock(key, { deptName, semester, period, schedule, departmentId, semesterType, rawPeriod });
} }
function renderRows(schedule) { function renderRows(schedule) {
if (!schedule || schedule.length === 0) { if (!Array.isArray(schedule) || schedule.length === 0) {
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>'; return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
} }
return schedule.map(r => ` return schedule.map(r => `
@@ -98,9 +123,9 @@ export async function initDepartment() {
<td>${escapeHtml(r.specialityCode || '-')}</td> <td>${escapeHtml(r.specialityCode || '-')}</td>
<td>${(() => { <td>${(() => {
const course = r.groupCourse || '-'; const course = r.groupCourse || '-';
const groupSemester = r.groupSemester || '-'; const semester = r.semester || '-';
if (course === '-' && groupSemester === '-') return '-'; if (course === '-' && semester === '-') return '-';
return `${course} | ${groupSemester}`; return `${course} | ${semester}`;
})()}</td> })()}</td>
<td>${escapeHtml(r.groupName || '-')}</td> <td>${escapeHtml(r.groupName || '-')}</td>
<td>${escapeHtml(r.subjectName || '-')}</td> <td>${escapeHtml(r.subjectName || '-')}</td>
@@ -117,6 +142,32 @@ export async function initDepartment() {
`).join(''); `).join('');
} }
// ===== Persistence: sessionStorage (fix #4) =====
function saveScheduleBlock(key, blockData) {
try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
stored[key] = blockData;
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
} catch (e) {
console.warn('Ошибка сохранения в sessionStorage:', e);
}
}
function restoreScheduleBlocks() {
try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
const keys = Object.keys(stored);
if (keys.length === 0) return;
keys.forEach(key => {
const b = stored[key];
renderScheduleBlock(b.deptName, b.semester, b.period, b.schedule, b.departmentId, b.semesterType, b.rawPeriod);
});
} catch (e) {
console.warn('Ошибка восстановления из sessionStorage:', e);
}
}
// ========================================================= // =========================================================
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)" // ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
// Два модальных окна поверх всего контента в одном оверлее // Два модальных окна поверх всего контента в одном оверлее
@@ -158,12 +209,28 @@ export async function initDepartment() {
csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' + csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join(''); csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
// Загрузка преподавателей: сначала по кафедре, при ошибке — все преподаватели
csTeachers = [];
if (localDepartmentId) { if (localDepartmentId) {
try {
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`); csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
} catch (e) {
console.warn('Не удалось загрузить преподавателей для кафедры, загружаем всех:', e);
}
}
// Фолбэк: загружаем всех преподавателей
if (!Array.isArray(csTeachers) || csTeachers.length === 0) {
try {
csTeachers = await api.get('/api/users/teachers');
} catch (e2) {
console.error('Ошибка загрузки всех преподавателей:', e2);
}
}
if (Array.isArray(csTeachers) && csTeachers.length > 0) {
csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' + csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join(''); csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
} else { } else {
csTeacherSelect.innerHTML = '<option value="">Ошибка: Не найден ID кафедры</option>'; csTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
} }
} catch (e) { } catch (e) {
console.error('Ошибка загрузки справочников:', e); console.error('Ошибка загрузки справочников:', e);
@@ -175,7 +242,7 @@ export async function initDepartment() {
// ===== Открытие / Закрытие оверлея ===== // ===== Открытие / Закрытие оверлея =====
function openOverlay() { function openOverlay() {
csOverlay.classList.add('open'); csOverlay.classList.add('open');
document.body.style.overflow = 'hidden'; // Предотвращаем скролл страницы document.body.style.overflow = 'hidden';
} }
function closeOverlay() { function closeOverlay() {
@@ -204,7 +271,6 @@ export async function initDepartment() {
modalCreateScheduleClose.addEventListener('click', closeOverlay); modalCreateScheduleClose.addEventListener('click', closeOverlay);
csOverlay.addEventListener('click', (e) => { csOverlay.addEventListener('click', (e) => {
// Закрыть по клику на затемнённый фон (но не по клику на содержимое модалок)
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) { if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
closeOverlay(); closeOverlay();
} }
@@ -216,10 +282,10 @@ export async function initDepartment() {
} }
}); });
// ===== Рендер таблицы ===== // ===== Рендер таблицы подготовленных записей =====
function renderPreparedSchedules() { function renderPreparedSchedules() {
if (preparedSchedules.length === 0) { if (preparedSchedules.length === 0) {
preparedSchedulesTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет записей</td></tr>'; preparedSchedulesTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет записей</td></tr>';
return; return;
} }
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => { preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
@@ -230,14 +296,13 @@ export async function initDepartment() {
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно'; const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType; const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
const periodDisplay = s.period.replace('-', '/'); const periodDisplay = s.period.replace('-', '/');
const divText = s.division ? '✓' : ''; const divText = s.isDivision ? '✓' : '';
const hasError = !!s._errorMsg; const hasError = !!s._errorMsg;
const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : ''; const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : '';
let row = ` let row = `
<tr${rowStyle}> <tr${rowStyle}>
<td>${escapeHtml(periodDisplay)}</td> <td>${escapeHtml(periodDisplay)}</td>
<td>${escapeHtml(semLabel)}</td> <td>${escapeHtml(semLabel)}</td>
<td>${s.groupSemester}</td>
<td>${escapeHtml(String(groupName))}</td> <td>${escapeHtml(String(groupName))}</td>
<td>${escapeHtml(String(subjectName))}</td> <td>${escapeHtml(String(subjectName))}</td>
<td>${escapeHtml(lessonTypeName)}</td> <td>${escapeHtml(lessonTypeName)}</td>
@@ -248,7 +313,7 @@ export async function initDepartment() {
</tr>`; </tr>`;
if (hasError) { if (hasError) {
row += `<tr style="background: rgba(239, 68, 68, 0.05);"> row += `<tr style="background: rgba(239, 68, 68, 0.05);">
<td colspan="10" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;"> <td colspan="9" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
${escapeHtml(s._errorMsg)} ${escapeHtml(s._errorMsg)}
</td> </td>
</tr>`; </tr>`;
@@ -268,8 +333,6 @@ export async function initDepartment() {
}); });
// ===== Очистка полей формы (частичная) ===== // ===== Очистка полей формы (частичная) =====
// НЕ очищаем select'ы — они остаются заполненными для удобства.
// Пользователь сам изменит нужные поля для следующей записи.
function clearFormFields() { function clearFormFields() {
document.getElementById('cs-hours').value = ''; document.getElementById('cs-hours').value = '';
document.getElementById('cs-division').checked = false; document.getElementById('cs-division').checked = false;
@@ -283,12 +346,11 @@ export async function initDepartment() {
const depId = csDepartmentIdInput.value; const depId = csDepartmentIdInput.value;
const period = document.getElementById('cs-period').value; const period = document.getElementById('cs-period').value;
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value; const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value;
const groupSemester = document.getElementById('cs-semester').value;
const groupId = csGroupSelect.value; const groupId = csGroupSelect.value;
const subjectId = csSubjectSelect.value; const subjectId = csSubjectSelect.value;
const lessonTypeId = document.getElementById('cs-lesson-type').value; const lessonTypeId = document.getElementById('cs-lesson-type').value;
const hours = document.getElementById('cs-hours').value; const hours = document.getElementById('cs-hours').value;
const division = document.getElementById('cs-division').checked; const isDivision = document.getElementById('cs-division').checked;
const teacherId = csTeacherSelect.value; const teacherId = csTeacherSelect.value;
if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) { if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
@@ -298,27 +360,25 @@ export async function initDepartment() {
const newRecord = { const newRecord = {
departmentId: Number(depId), departmentId: Number(depId),
groupSemester: Number(groupSemester),
groupId: Number(groupId), groupId: Number(groupId),
subjectsId: Number(subjectId), subjectsId: Number(subjectId),
lessonTypeId: Number(lessonTypeId), lessonTypeId: Number(lessonTypeId),
numberOfHours: Number(hours), numberOfHours: Number(hours),
division: division, isDivision: isDivision,
teacherId: Number(teacherId), teacherId: Number(teacherId),
semesterType: semesterType, semesterType: semesterType,
period: period period: period
}; };
// Проверка на дубликат в уже добавленных записях // Проверка на дубликат
const isDuplicate = preparedSchedules.some(s => const isDuplicate = preparedSchedules.some(s =>
s.period === newRecord.period && s.period === newRecord.period &&
s.semesterType === newRecord.semesterType && s.semesterType === newRecord.semesterType &&
s.groupSemester === newRecord.groupSemester &&
s.groupId === newRecord.groupId && s.groupId === newRecord.groupId &&
s.subjectsId === newRecord.subjectsId && s.subjectsId === newRecord.subjectsId &&
s.lessonTypeId === newRecord.lessonTypeId && s.lessonTypeId === newRecord.lessonTypeId &&
s.numberOfHours === newRecord.numberOfHours && s.numberOfHours === newRecord.numberOfHours &&
s.division === newRecord.division && s.isDivision === newRecord.isDivision &&
s.teacherId === newRecord.teacherId s.teacherId === newRecord.teacherId
); );
@@ -332,7 +392,7 @@ export async function initDepartment() {
clearFormFields(); clearFormFields();
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success'); showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
setTimeout(() => hideAlert('create-schedule-alert'), 2000); setTimeout(() => hideAlert('create-schedule-alert'), 4000); // fix #1: 4 секунды
renderPreparedSchedules(); renderPreparedSchedules();
updateTableVisibility(); updateTableVisibility();
@@ -360,7 +420,6 @@ export async function initDepartment() {
} catch (err) { } catch (err) {
console.error('Ошибка сохранения записи:', err); console.error('Ошибка сохранения записи:', err);
errors++; errors++;
// Помечаем запись как дубликат, если бэк вернул соответствующую ошибку
const isDuplicate = err.status === 409 || const isDuplicate = err.status === 409 ||
(err.message && err.message.toLowerCase().includes('уже существует')); (err.message && err.message.toLowerCase().includes('уже существует'));
failedRecords.push({ failedRecords.push({
@@ -382,7 +441,6 @@ export async function initDepartment() {
updateTableVisibility(); updateTableVisibility();
setTimeout(closeOverlay, 2000); setTimeout(closeOverlay, 2000);
} else { } else {
// Оставляем неудачные записи для повторной попытки / удаления
preparedSchedules = failedRecords; preparedSchedules = failedRecords;
renderPreparedSchedules(); renderPreparedSchedules();
if (saved > 0) { if (saved > 0) {

View File

@@ -17,7 +17,7 @@ export async function initGroups() {
populateEfSelects(educationForms); populateEfSelects(educationForms);
await loadGroups(); await loadGroups();
} catch (e) { } catch (e) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки данных</td></tr>'; groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки данных</td></tr>';
} }
} }
@@ -26,7 +26,7 @@ export async function initGroups() {
allGroups = await api.get('/api/groups'); allGroups = await api.get('/api/groups');
applyGroupFilter(); applyGroupFilter();
} catch (e) { } catch (e) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки</td></tr>'; groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки</td></tr>';
} }
} }
@@ -61,7 +61,7 @@ export async function initGroups() {
function renderGroups(groups) { function renderGroups(groups) {
if (!groups || !groups.length) { if (!groups || !groups.length) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Нет групп</td></tr>'; groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет групп</td></tr>';
return; return;
} }
groupsTbody.innerHTML = groups.map(g => ` groupsTbody.innerHTML = groups.map(g => `
@@ -72,6 +72,7 @@ export async function initGroups() {
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td> <td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
<td>${g.departmentId || '-'}</td> <td>${g.departmentId || '-'}</td>
<td>${g.course || '-'}</td> <td>${g.course || '-'}</td>
<td>${escapeHtml(g.specialityCode || '-')}</td>
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td> <td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
</tr>`).join(''); </tr>`).join('');
} }
@@ -83,13 +84,15 @@ export async function initGroups() {
const groupSize = document.getElementById('new-group-size').value; const groupSize = document.getElementById('new-group-size').value;
const educationFormId = newGroupEfSelect.value; const educationFormId = newGroupEfSelect.value;
const departmentId = document.getElementById('new-group-department').value; const departmentId = document.getElementById('new-group-department').value;
const yearStartStudy = document.getElementById('new-group-yearStartStudy').value; const course = document.getElementById('new-group-course').value;
const specialityCode = document.getElementById('new-group-speciality-code').value.trim();
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; } if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; } if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; } if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; } if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
if (!yearStartStudy) { showAlert('create-group-alert', 'Введите курс', 'error'); return; } if (!course) { showAlert('create-group-alert', 'Введите курс', 'error'); return; }
if (!specialityCode) { showAlert('create-group-alert', 'Введите код специальности', 'error'); return; }
try { try {
const data = await api.post('/api/groups', { const data = await api.post('/api/groups', {
@@ -97,7 +100,8 @@ export async function initGroups() {
groupSize: Number(groupSize), groupSize: Number(groupSize),
educationFormId: Number(educationFormId), educationFormId: Number(educationFormId),
departmentId: Number(departmentId), departmentId: Number(departmentId),
yearStartStudy: Number(yearStartStudy) course: Number(course),
specialityCode: specialityCode
}); });
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success'); showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
createGroupForm.reset(); createGroupForm.reset();

View File

@@ -30,11 +30,11 @@
<label for="filter-period">Учебный год</label> <label for="filter-period">Учебный год</label>
<select id="filter-period" required> <select id="filter-period" required>
<option value="">Выберите...</option> <option value="">Выберите...</option>
<option value="2022-2023">2022/2023</option>
<option value="2023-2024">2023/2024</option>
<option value="2024-2025">2024/2025</option>
<option value="2025-2026">2025/2026</option>
<option value="2026-2027">2026/2027</option> <option value="2026-2027">2026/2027</option>
<option value="2025-2026">2025/2026</option>
<option value="2024-2025">2024/2025</option>
<option value="2023-2024">2023/2024</option>
<option value="2022-2023">2022/2023</option>
</select> </select>
</div> </div>
@@ -63,9 +63,9 @@
<label for="cs-period">Учебный год</label> <label for="cs-period">Учебный год</label>
<select id="cs-period" required> <select id="cs-period" required>
<option value="">Выберите...</option> <option value="">Выберите...</option>
<option value="2024-2025">2024/2025</option>
<option value="2025-2026">2025/2026</option>
<option value="2026-2027">2026/2027</option> <option value="2026-2027">2026/2027</option>
<option value="2025-2026">2025/2026</option>
<option value="2024-2025">2024/2025</option>
</select> </select>
</div> </div>
@@ -83,11 +83,6 @@
</div> </div>
</div> </div>
<div class="form-group" style="flex: 1 1 150px;">
<label for="cs-semester">Курс/Семестр (номер)</label>
<input type="number" id="cs-semester" required min="1" max="12" placeholder="Например: 1">
</div>
<div class="form-group" style="flex: 1 1 180px;"> <div class="form-group" style="flex: 1 1 180px;">
<label for="cs-group">Группа</label> <label for="cs-group">Группа</label>
<select id="cs-group" required> <select id="cs-group" required>
@@ -159,7 +154,6 @@
<tr> <tr>
<th>Уч. год</th> <th>Уч. год</th>
<th>Семестр</th> <th>Семестр</th>
<th></th>
<th>Группа</th> <th>Группа</th>
<th>Дисциплина</th> <th>Дисциплина</th>
<th>Вид</th> <th>Вид</th>
@@ -171,7 +165,7 @@
</thead> </thead>
<tbody id="prepared-schedules-tbody"> <tbody id="prepared-schedules-tbody">
<tr> <tr>
<td colspan="10" class="loading-row">Нет записей</td> <td colspan="9" class="loading-row">Нет записей</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -25,6 +25,10 @@
<label for="new-group-yearStartStudy">Год начала обучения</label> <label for="new-group-yearStartStudy">Год начала обучения</label>
<input type="number" id="new-group-yearStartStudy" required pattern="^20\d{2}$" maxlength="3" placeholder="2026"> <input type="number" id="new-group-yearStartStudy" required pattern="^20\d{2}$" maxlength="3" placeholder="2026">
</div> </div>
<div class="form-group">
<label for="new-group-speciality-code">Код специальности</label>
<input type="text" id="new-group-speciality-code" placeholder="09.03.01" required>
</div>
<button type="submit" class="btn-primary">Создать</button> <button type="submit" class="btn-primary">Создать</button>
</div> </div>
<div class="form-alert" id="create-group-alert" role="alert"></div> <div class="form-alert" id="create-group-alert" role="alert"></div>
@@ -51,12 +55,13 @@
<th>Форма обучения</th> <th>Форма обучения</th>
<th>ID кафедры</th> <th>ID кафедры</th>
<th>Курс</th> <th>Курс</th>
<th>Код специальности</th>
<th>Действия</th> <th>Действия</th>
</tr> </tr>
</thead> </thead>
<tbody id="groups-tbody"> <tbody id="groups-tbody">
<tr> <tr>
<td colspan="7" class="loading-row">Загрузка...</td> <td colspan="8" class="loading-row">Загрузка...</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>