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 = `
@@ -75,7 +66,6 @@ export async function initDepartment() {
${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 @@
-
Запрос расписания кафедры
+
+
Запрос расписания кафедры
+
+