diff --git a/backend/src/main/java/com/magistr/app/controller/AuthController.java b/backend/src/main/java/com/magistr/app/controller/AuthController.java index 1dfc482..14c7f2a 100755 --- a/backend/src/main/java/com/magistr/app/controller/AuthController.java +++ b/backend/src/main/java/com/magistr/app/controller/AuthController.java @@ -38,14 +38,15 @@ public class AuthController { !passwordEncoder.matches(request.getPassword(), userOpt.get().getPassword())) { return ResponseEntity .status(401) - .body(new LoginResponse(false, "Неверное имя пользователя или пароль", null, null, null)); + .body(new LoginResponse(false, "Неверное имя пользователя или пароль", null, null, null, null)); } User user = userOpt.get(); String token = UUID.randomUUID().toString(); String roleName = user.getRole().name(); String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/"); + Long departmentId = user.getDepartmentId(); - return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect)); + return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId)); } } diff --git a/backend/src/main/java/com/magistr/app/dto/LoginResponse.java b/backend/src/main/java/com/magistr/app/dto/LoginResponse.java index 7fa87cd..851619b 100755 --- a/backend/src/main/java/com/magistr/app/dto/LoginResponse.java +++ b/backend/src/main/java/com/magistr/app/dto/LoginResponse.java @@ -7,16 +7,18 @@ public class LoginResponse { private String token; private String role; private String redirect; + private Long departmentId; public LoginResponse() { } - public LoginResponse(boolean success, String message, String token, String role, String redirect) { + public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId) { this.success = success; this.message = message; this.token = token; this.role = role; this.redirect = redirect; + this.departmentId = departmentId; } public boolean isSuccess() { @@ -58,4 +60,12 @@ public class LoginResponse { public void setRedirect(String redirect) { this.redirect = redirect; } + + public Long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(Long departmentId) { + this.departmentId = departmentId; + } } diff --git a/frontend/admin/css/department.css b/frontend/admin/css/department.css index d39a250..93c8913 100644 --- a/frontend/admin/css/department.css +++ b/frontend/admin/css/department.css @@ -1,3 +1,82 @@ +/* ===== Оверлей для модалок создания записей (к/ф) ===== */ +.cs-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.cs-overlay.open { + display: block; +} + +.cs-overlay-scroll { + width: 100%; + height: 100%; + overflow-y: auto; + padding: 2rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +/* Общие стили для обеих модалок */ +.cs-modal { + width: 100%; + max-width: 1100px; + position: relative; + animation: csModalAppear 0.25s ease-out; +} + +/* Модалка 1 (форма) всегда поверх модалки 2 (таблицы), + чтобы выпадающие списки не уходили под таблицу */ +.cs-modal-form { + z-index: 2; +} + +.cs-modal-table { + z-index: 1; +} + +@keyframes csModalAppear { + from { opacity: 0; transform: translateY(-12px); } + to { opacity: 1; transform: translateY(0); } +} + +.cs-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.cs-modal-header h2 { + margin: 0; +} + +/* Кнопка закрытия */ +.btn-close-panel { + background: none; + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + font-size: 1.3rem; + line-height: 1; + padding: 0.25rem 0.6rem; + color: var(--text-secondary); + cursor: pointer; + transition: color var(--transition), background var(--transition), border-color var(--transition); +} + +.btn-close-panel:hover { + color: var(--error); + background: rgba(239, 68, 68, 0.1); + border-color: var(--error); +} + .wrap{ max-width: 900px; margin: 0 auto; diff --git a/frontend/admin/js/views/department.js b/frontend/admin/js/views/department.js index 63e6388..191ef01 100644 --- a/frontend/admin/js/views/department.js +++ b/frontend/admin/js/views/department.js @@ -11,7 +11,7 @@ export async function initDepartment() { // Загрузка кафедр try { departments = await api.get('/api/departments'); - departmentSelect.innerHTML = '' + + departmentSelect.innerHTML = '' + departments.map(d => ``).join(''); } catch (e) { departmentSelect.innerHTML = ''; @@ -33,22 +33,14 @@ export async function initDepartment() { const deptName = departmentSelect.options[departmentSelect.selectedIndex].text; try { - const params = new URLSearchParams({ - departmentId, - semesterType, - period - }); - - // Запрос на бэк + const params = new URLSearchParams({ departmentId, semesterType, period }); const data = await api.get(`/api/department/schedule?${params.toString()}`); - - const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType); - const periodName = period.replace('-', '/'); // Display 2024-2025 as 2024/2025 - - renderScheduleBlock(deptName, semesterName, periodName, data); - - form.reset(); + const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType); + const periodName = period.replace('-', '/'); + + renderScheduleBlock(deptName, semesterName, periodName, data); + form.reset(); } catch (err) { showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error'); } @@ -57,8 +49,7 @@ export async function initDepartment() { function renderScheduleBlock(deptName, semester, period, schedule) { const details = document.createElement('details'); details.className = 'table-item'; - details.open = true; // Сразу открываем новый блок - + details.open = true; details.innerHTML = `
${schedule ? schedule.length : 0} записей
-
@@ -96,7 +86,6 @@ export async function initDepartment() {
`; - container.prepend(details); } @@ -104,34 +93,306 @@ export async function initDepartment() { if (!schedule || schedule.length === 0) { return 'Нет данных'; } - return schedule.map(r => ` ${escapeHtml(r.specialityCode || '-')} - - ${(() => { - const course = r.groupCourse || '-'; - const semester = r.semester || '-'; - if (course === '-' && semester === '-') return '-'; - return `${course} | ${semester}`; - })()} - + ${(() => { + const course = r.groupCourse || '-'; + const semester = r.semester || '-'; + if (course === '-' && semester === '-') return '-'; + return `${course} | ${semester}`; + })()} ${escapeHtml(r.groupName || '-')} ${escapeHtml(r.subjectName || '-')} ${escapeHtml(r.lessonType || '-')} ${escapeHtml(r.numberOfHours || '-')} - - ${r.division === true ? '✓' : (r.division === false ? '' : escapeHtml(''))} - - - ${(() => { - const jobTitle = r.teacherJobTitle || '-'; - const teacherName = r.teacherName || '-'; - if (jobTitle === '-' && teacherName === '-') return '-'; - return `${jobTitle}, ${teacherName}`; - })()} - + ${r.division === true ? '✓' : ''} + ${(() => { + const jobTitle = r.teacherJobTitle || '-'; + const teacherName = r.teacherName || '-'; + if (jobTitle === '-' && teacherName === '-') return '-'; + return `${jobTitle}, ${teacherName}`; + })()} `).join(''); } + + // ========================================================= + // ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)" + // Два модальных окна поверх всего контента в одном оверлее + // ========================================================= + const btnCreateSchedule = document.getElementById('btn-create-schedule'); + const csOverlay = document.getElementById('cs-overlay'); + + const modalCreateSchedule = document.getElementById('modal-create-schedule'); + const modalCreateScheduleClose = document.getElementById('modal-create-schedule-close'); + const formCreateSchedule = document.getElementById('create-schedule-form'); + + const modalViewSchedules = document.getElementById('modal-view-schedules'); + const btnSaveSchedules = document.getElementById('btn-save-schedules'); + const preparedSchedulesTbody = document.getElementById('prepared-schedules-tbody'); + + const csGroupSelect = document.getElementById('cs-group'); + const csSubjectSelect = document.getElementById('cs-subject'); + const csTeacherSelect = document.getElementById('cs-teacher'); + const csDepartmentIdInput = document.getElementById('cs-department-id'); + + let preparedSchedules = []; + let csGroups = []; + let csSubjects = []; + let csTeachers = []; + + const SEMESTER_LABELS = { autumn: 'Осенний', spring: 'Весенний' }; + const LESSON_TYPE_LABELS = { 1: 'Лекция', 2: 'Практическая работа', 3: 'Лабораторная работа' }; + + const localDepartmentId = localStorage.getItem('departmentId'); + + // ===== Загрузка справочников ===== + async function loadDictionariesForSchedule() { + try { + csGroups = await api.get('/api/groups'); + csGroupSelect.innerHTML = '' + + csGroups.map(g => ``).join(''); + + csSubjects = await api.get('/api/subjects'); + csSubjectSelect.innerHTML = '' + + csSubjects.map(s => ``).join(''); + + if (localDepartmentId) { + csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`); + csTeacherSelect.innerHTML = '' + + csTeachers.map(t => ``).join(''); + } else { + csTeacherSelect.innerHTML = ''; + } + } catch (e) { + console.error('Ошибка загрузки справочников:', e); + } + } + + loadDictionariesForSchedule(); + + // ===== Открытие / Закрытие оверлея ===== + function openOverlay() { + csOverlay.classList.add('open'); + document.body.style.overflow = 'hidden'; // Предотвращаем скролл страницы + } + + function closeOverlay() { + csOverlay.classList.remove('open'); + document.body.style.overflow = ''; + hideAlert('create-schedule-alert'); + hideAlert('save-schedules-alert'); + } + + function updateTableVisibility() { + modalViewSchedules.style.display = preparedSchedules.length > 0 ? '' : 'none'; + } + + // ===== Кнопка «Создать запись» ===== + btnCreateSchedule.addEventListener('click', () => { + if (localDepartmentId) { + csDepartmentIdInput.value = localDepartmentId; + } else { + showAlert('schedule-form-alert', 'Требуется перезайти (отсутствует ID кафедры)', 'error'); + return; + } + openOverlay(); + }); + + // ===== Закрытие ===== + modalCreateScheduleClose.addEventListener('click', closeOverlay); + + csOverlay.addEventListener('click', (e) => { + // Закрыть по клику на затемнённый фон (но не по клику на содержимое модалок) + if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) { + closeOverlay(); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && csOverlay.classList.contains('open')) { + closeOverlay(); + } + }); + + // ===== Рендер таблицы ===== + function renderPreparedSchedules() { + if (preparedSchedules.length === 0) { + preparedSchedulesTbody.innerHTML = 'Нет записей'; + return; + } + preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => { + const groupName = csGroups.find(g => g.id == s.groupId)?.name || s.groupId; + const subjectName = csSubjects.find(sub => sub.id == s.subjectsId)?.name || s.subjectsId; + const teacherName = csTeachers.find(t => t.id == s.teacherId)?.fullName + || csTeachers.find(t => t.id == s.teacherId)?.username || s.teacherId; + const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно'; + const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType; + const periodDisplay = s.period.replace('-', '/'); + const divText = s.isDivision ? '✓' : ''; + const hasError = !!s._errorMsg; + const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : ''; + let row = ` + + ${escapeHtml(periodDisplay)} + ${escapeHtml(semLabel)} + ${s.semester} + ${escapeHtml(String(groupName))} + ${escapeHtml(String(subjectName))} + ${escapeHtml(lessonTypeName)} + ${s.numberOfHours} + ${divText} + ${escapeHtml(String(teacherName))} + + `; + if (hasError) { + row += ` + + ⚠ ${escapeHtml(s._errorMsg)} + + `; + } + return row; + }).join(''); + } + + // ===== Удаление строки из таблицы ===== + preparedSchedulesTbody.addEventListener('click', (e) => { + if (e.target.classList.contains('btn-delete')) { + const idx = parseInt(e.target.getAttribute('data-index'), 10); + preparedSchedules.splice(idx, 1); + renderPreparedSchedules(); + updateTableVisibility(); + } + }); + + // ===== Очистка полей формы (частичная) ===== + // НЕ очищаем select'ы — они остаются заполненными для удобства. + // Пользователь сам изменит нужные поля для следующей записи. + function clearFormFields() { + document.getElementById('cs-hours').value = ''; + document.getElementById('cs-division').checked = false; + } + + // ===== Добавление записи в список ===== + formCreateSchedule.addEventListener('submit', (e) => { + e.preventDefault(); + hideAlert('create-schedule-alert'); + + const depId = csDepartmentIdInput.value; + const period = document.getElementById('cs-period').value; + const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value; + const semester = document.getElementById('cs-semester').value; + const groupId = csGroupSelect.value; + const subjectId = csSubjectSelect.value; + const lessonTypeId = document.getElementById('cs-lesson-type').value; + const hours = document.getElementById('cs-hours').value; + const isDivision = document.getElementById('cs-division').checked; + const teacherId = csTeacherSelect.value; + + if (!period || !semesterType || !semester || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) { + showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error'); + return; + } + + const newRecord = { + departmentId: Number(depId), + semester: Number(semester), + groupId: Number(groupId), + subjectsId: Number(subjectId), + lessonTypeId: Number(lessonTypeId), + numberOfHours: Number(hours), + isDivision: isDivision, + teacherId: Number(teacherId), + semesterType: semesterType, + period: period + }; + + // Проверка на дубликат в уже добавленных записях + const isDuplicate = preparedSchedules.some(s => + s.period === newRecord.period && + s.semesterType === newRecord.semesterType && + s.semester === newRecord.semester && + s.groupId === newRecord.groupId && + s.subjectsId === newRecord.subjectsId && + s.lessonTypeId === newRecord.lessonTypeId && + s.numberOfHours === newRecord.numberOfHours && + s.isDivision === newRecord.isDivision && + s.teacherId === newRecord.teacherId + ); + + if (isDuplicate) { + showAlert('create-schedule-alert', 'Такая запись уже есть в списке', 'error'); + return; + } + + preparedSchedules.push(newRecord); + + clearFormFields(); + + showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success'); + setTimeout(() => hideAlert('create-schedule-alert'), 2000); + + renderPreparedSchedules(); + updateTableVisibility(); + }); + + // ===== Сохранение в БД ===== + btnSaveSchedules.addEventListener('click', async () => { + if (preparedSchedules.length === 0) { + showAlert('save-schedules-alert', 'Нет записей для сохранения', 'error'); + return; + } + + btnSaveSchedules.disabled = true; + btnSaveSchedules.textContent = 'Сохранение...'; + hideAlert('save-schedules-alert'); + + let errors = 0; + let saved = 0; + const failedRecords = []; + + for (const record of preparedSchedules) { + try { + await api.post('/api/department/schedule/create', record); + saved++; + } catch (err) { + console.error('Ошибка сохранения записи:', err); + errors++; + // Помечаем запись как дубликат, если бэк вернул соответствующую ошибку + const isDuplicate = err.status === 409 || + (err.message && err.message.toLowerCase().includes('уже существует')); + failedRecords.push({ + ...record, + _errorMsg: isDuplicate + ? 'Такая запись уже есть в базе данных' + : (err.message || 'Ошибка сохранения') + }); + } + } + + btnSaveSchedules.disabled = false; + btnSaveSchedules.textContent = 'Сохранить в БД'; + + if (errors === 0) { + showAlert('save-schedules-alert', `Все записи (${saved}) успешно сохранены!`, 'success'); + preparedSchedules = []; + renderPreparedSchedules(); + updateTableVisibility(); + setTimeout(closeOverlay, 2000); + } else { + // Оставляем неудачные записи для повторной попытки / удаления + preparedSchedules = failedRecords; + renderPreparedSchedules(); + if (saved > 0) { + showAlert('save-schedules-alert', + `Сохранено: ${saved}. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error'); + } else { + showAlert('save-schedules-alert', + `Не удалось сохранить. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error'); + } + } + }); + } \ No newline at end of file diff --git a/frontend/admin/js/views/users.js b/frontend/admin/js/views/users.js index 0abfae6..e77ee8c 100755 --- a/frontend/admin/js/views/users.js +++ b/frontend/admin/js/views/users.js @@ -383,7 +383,7 @@ export async function initUsers() { const role = document.getElementById('new-role').value; const fullName = document.getElementById('new-fullname').value.trim(); const jobTitle = document.getElementById('new-jobtitle').value.trim(); - const department = document.getElementById('new-department').value; + const departmentId = document.getElementById('new-department').value; if (!username || !password || !fullName || !jobTitle || !departmentId) { showAlert('create-alert', 'Заполните все поля', 'error'); diff --git a/frontend/admin/views/department.html b/frontend/admin/views/department.html index c86cb66..d0b435e 100644 --- a/frontend/admin/views/department.html +++ b/frontend/admin/views/department.html @@ -1,5 +1,8 @@
-

Запрос расписания кафедры

+
+

Запрос расписания кафедры

+ +
@@ -12,14 +15,14 @@
- - + +
@@ -41,6 +44,143 @@
+ +
+
+ + + + + + + +
+
+
\ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js index d2ed480..80bdf31 100755 --- a/frontend/script.js +++ b/frontend/script.js @@ -143,6 +143,7 @@ if (data.token) localStorage.setItem('token', data.token); if (data.role) localStorage.setItem('role', data.role); + if (data.departmentId) localStorage.setItem('departmentId', data.departmentId); const redirect = data.redirect || '/'; setTimeout(() => { window.location.href = redirect; }, 400);