доработал отображение учебного графика
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
| `specialties` | Коды и названия специальностей |
|
| `specialties` | Коды и названия специальностей |
|
||||||
| `specialty_profiles` | Профили обучения внутри специальности |
|
| `specialty_profiles` | Профили обучения внутри специальности |
|
||||||
| `student_groups` | Группы со ссылками на специальность и профиль |
|
| `student_groups` | Группы со ссылками на специальность и профиль |
|
||||||
| `calendar_study_forms` | Формы обучения календарного графика |
|
| `education_forms` | Общий справочник форм обучения для групп и календарных графиков |
|
||||||
| `academic_calendar_activity_types` | Коды активностей из Excel-графика |
|
| `academic_calendar_activity_types` | Коды активностей из Excel-графика |
|
||||||
| `academic_calendars` | Календарные графики профиля, формы обучения и учебного года |
|
| `academic_calendars` | Календарные графики профиля, формы обучения и учебного года |
|
||||||
| `academic_calendar_days` | Дневная сетка графика по курсу и дате |
|
| `academic_calendar_days` | Дневная сетка графика по курсу и дате |
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class AcademicCalendarController {
|
|||||||
private final AcademicYearRepository academicYearRepository;
|
private final AcademicYearRepository academicYearRepository;
|
||||||
private final SpecialtiesRepository specialtiesRepository;
|
private final SpecialtiesRepository specialtiesRepository;
|
||||||
private final SpecialtyProfileRepository profileRepository;
|
private final SpecialtyProfileRepository profileRepository;
|
||||||
private final CalendarStudyFormRepository studyFormRepository;
|
private final EducationFormRepository educationFormRepository;
|
||||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||||
|
|
||||||
public AcademicCalendarController(AcademicCalendarRepository calendarRepository,
|
public AcademicCalendarController(AcademicCalendarRepository calendarRepository,
|
||||||
@@ -30,7 +30,7 @@ public class AcademicCalendarController {
|
|||||||
AcademicYearRepository academicYearRepository,
|
AcademicYearRepository academicYearRepository,
|
||||||
SpecialtiesRepository specialtiesRepository,
|
SpecialtiesRepository specialtiesRepository,
|
||||||
SpecialtyProfileRepository profileRepository,
|
SpecialtyProfileRepository profileRepository,
|
||||||
CalendarStudyFormRepository studyFormRepository,
|
EducationFormRepository educationFormRepository,
|
||||||
ScheduleGeneratorService scheduleGeneratorService) {
|
ScheduleGeneratorService scheduleGeneratorService) {
|
||||||
this.calendarRepository = calendarRepository;
|
this.calendarRepository = calendarRepository;
|
||||||
this.calendarDayRepository = calendarDayRepository;
|
this.calendarDayRepository = calendarDayRepository;
|
||||||
@@ -38,7 +38,7 @@ public class AcademicCalendarController {
|
|||||||
this.academicYearRepository = academicYearRepository;
|
this.academicYearRepository = academicYearRepository;
|
||||||
this.specialtiesRepository = specialtiesRepository;
|
this.specialtiesRepository = specialtiesRepository;
|
||||||
this.profileRepository = profileRepository;
|
this.profileRepository = profileRepository;
|
||||||
this.studyFormRepository = studyFormRepository;
|
this.educationFormRepository = educationFormRepository;
|
||||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,13 +51,6 @@ public class AcademicCalendarController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/study-forms")
|
|
||||||
public List<CalendarStudyFormDto> getStudyForms() {
|
|
||||||
return studyFormRepository.findAllByOrderByNameAsc().stream()
|
|
||||||
.map(form -> new CalendarStudyFormDto(form.getId(), form.getName(), form.getDescription()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<?> getCalendar(@PathVariable Long id) {
|
public ResponseEntity<?> getCalendar(@PathVariable Long id) {
|
||||||
return calendarRepository.findByIdWithDetails(id)
|
return calendarRepository.findByIdWithDetails(id)
|
||||||
@@ -165,8 +158,8 @@ public class AcademicCalendarController {
|
|||||||
if (!profile.getSpeciality().getId().equals(speciality.getId())) {
|
if (!profile.getSpeciality().getId().equals(speciality.getId())) {
|
||||||
throw new IllegalArgumentException("Профиль не относится к выбранной специальности");
|
throw new IllegalArgumentException("Профиль не относится к выбранной специальности");
|
||||||
}
|
}
|
||||||
CalendarStudyForm studyForm = studyFormRepository.findById(request.studyFormId())
|
EducationForm studyForm = educationFormRepository.findById(request.studyFormId())
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Форма обучения графика не найдена"));
|
.orElseThrow(() -> new IllegalArgumentException("Форма обучения не найдена"));
|
||||||
|
|
||||||
calendar.setTitle(request.title().trim());
|
calendar.setTitle(request.title().trim());
|
||||||
calendar.setAcademicYear(year);
|
calendar.setAcademicYear(year);
|
||||||
@@ -218,7 +211,7 @@ public class AcademicCalendarController {
|
|||||||
AcademicYear year = calendar.getAcademicYear();
|
AcademicYear year = calendar.getAcademicYear();
|
||||||
Speciality speciality = calendar.getSpeciality();
|
Speciality speciality = calendar.getSpeciality();
|
||||||
SpecialtyProfile profile = calendar.getSpecialtyProfile();
|
SpecialtyProfile profile = calendar.getSpecialtyProfile();
|
||||||
CalendarStudyForm studyForm = calendar.getStudyForm();
|
EducationForm studyForm = calendar.getStudyForm();
|
||||||
return new AcademicCalendarDto(
|
return new AcademicCalendarDto(
|
||||||
calendar.getId(),
|
calendar.getId(),
|
||||||
calendar.getTitle(),
|
calendar.getTitle(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.magistr.app.controller;
|
|||||||
|
|
||||||
import com.magistr.app.model.EducationForm;
|
import com.magistr.app.model.EducationForm;
|
||||||
import com.magistr.app.model.StudentGroup;
|
import com.magistr.app.model.StudentGroup;
|
||||||
|
import com.magistr.app.repository.AcademicCalendarRepository;
|
||||||
import com.magistr.app.repository.EducationFormRepository;
|
import com.magistr.app.repository.EducationFormRepository;
|
||||||
import com.magistr.app.repository.GroupRepository;
|
import com.magistr.app.repository.GroupRepository;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -16,16 +17,19 @@ public class EducationFormController {
|
|||||||
|
|
||||||
private final EducationFormRepository educationFormRepository;
|
private final EducationFormRepository educationFormRepository;
|
||||||
private final GroupRepository groupRepository;
|
private final GroupRepository groupRepository;
|
||||||
|
private final AcademicCalendarRepository academicCalendarRepository;
|
||||||
|
|
||||||
public EducationFormController(EducationFormRepository educationFormRepository,
|
public EducationFormController(EducationFormRepository educationFormRepository,
|
||||||
GroupRepository groupRepository) {
|
GroupRepository groupRepository,
|
||||||
|
AcademicCalendarRepository academicCalendarRepository) {
|
||||||
this.educationFormRepository = educationFormRepository;
|
this.educationFormRepository = educationFormRepository;
|
||||||
this.groupRepository = groupRepository;
|
this.groupRepository = groupRepository;
|
||||||
|
this.academicCalendarRepository = academicCalendarRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<Map<String, Object>> getAll() {
|
public List<Map<String, Object>> getAll() {
|
||||||
return educationFormRepository.findAll().stream()
|
return educationFormRepository.findAllByOrderByNameAsc().stream()
|
||||||
.map(ef -> Map.<String, Object>of("id", ef.getId(), "name", ef.getName()))
|
.map(ef -> Map.<String, Object>of("id", ef.getId(), "name", ef.getName()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
@@ -54,12 +58,17 @@ public class EducationFormController {
|
|||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any groups use this education form
|
// Нельзя удалить форму обучения, пока она используется группами или графиками.
|
||||||
List<StudentGroup> linked = groupRepository.findByEducationFormId(id);
|
List<StudentGroup> linked = groupRepository.findByEducationFormId(id);
|
||||||
if (!linked.isEmpty()) {
|
if (!linked.isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"message", "Невозможно удалить: есть привязанные группы (" + linked.size() + ")"));
|
"message", "Невозможно удалить: есть привязанные группы (" + linked.size() + ")"));
|
||||||
}
|
}
|
||||||
|
long linkedCalendars = academicCalendarRepository.countByStudyFormId(id);
|
||||||
|
if (linkedCalendars > 0) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
|
"message", "Невозможно удалить: есть привязанные календарные графики (" + linkedCalendars + ")"));
|
||||||
|
}
|
||||||
|
|
||||||
educationFormRepository.deleteById(id);
|
educationFormRepository.deleteById(id);
|
||||||
return ResponseEntity.ok(Map.of("message", "Форма обучения удалена"));
|
return ResponseEntity.ok(Map.of("message", "Форма обучения удалена"));
|
||||||
|
|||||||
@@ -294,6 +294,9 @@ public class GroupController {
|
|||||||
|| !calendar.getSpecialtyProfile().getId().equals(group.getSpecialtyProfile().getId())) {
|
|| !calendar.getSpecialtyProfile().getId().equals(group.getSpecialtyProfile().getId())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует специальности и профилю группы"));
|
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует специальности и профилю группы"));
|
||||||
}
|
}
|
||||||
|
if (!calendar.getStudyForm().getId().equals(group.getEducationForm().getId())) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует форме обучения группы"));
|
||||||
|
}
|
||||||
|
|
||||||
StudentGroupCalendarAssignment assignment = assignmentRepository
|
StudentGroupCalendarAssignment assignment = assignmentRepository
|
||||||
.findByGroupIdAndAcademicYearIdWithDetails(group.getId(), academicYear.getId())
|
.findByGroupIdAndAcademicYearIdWithDetails(group.getId(), academicYear.getId())
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.magistr.app.dto;
|
|
||||||
|
|
||||||
public record CalendarStudyFormDto(
|
|
||||||
Long id,
|
|
||||||
String name,
|
|
||||||
String description
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ public class AcademicCalendar {
|
|||||||
|
|
||||||
@ManyToOne(optional = false)
|
@ManyToOne(optional = false)
|
||||||
@JoinColumn(name = "study_form_id", nullable = false)
|
@JoinColumn(name = "study_form_id", nullable = false)
|
||||||
private CalendarStudyForm studyForm;
|
private EducationForm studyForm;
|
||||||
|
|
||||||
@Column(name = "course_count", nullable = false)
|
@Column(name = "course_count", nullable = false)
|
||||||
private Integer courseCount;
|
private Integer courseCount;
|
||||||
@@ -72,11 +72,11 @@ public class AcademicCalendar {
|
|||||||
this.specialtyProfile = specialtyProfile;
|
this.specialtyProfile = specialtyProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CalendarStudyForm getStudyForm() {
|
public EducationForm getStudyForm() {
|
||||||
return studyForm;
|
return studyForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStudyForm(CalendarStudyForm studyForm) {
|
public void setStudyForm(EducationForm studyForm) {
|
||||||
this.studyForm = studyForm;
|
this.studyForm = studyForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.magistr.app.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "calendar_study_forms")
|
|
||||||
public class CalendarStudyForm {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(nullable = false, unique = true, length = 100)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDescription(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long> {
|
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long> {
|
||||||
|
|
||||||
|
long countByStudyFormId(Long studyFormId);
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
select calendar
|
select calendar
|
||||||
from AcademicCalendar calendar
|
from AcademicCalendar calendar
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.magistr.app.repository;
|
|
||||||
|
|
||||||
import com.magistr.app.model.CalendarStudyForm;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface CalendarStudyFormRepository extends JpaRepository<CalendarStudyForm, Long> {
|
|
||||||
|
|
||||||
List<CalendarStudyForm> findAllByOrderByNameAsc();
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,12 @@ package com.magistr.app.repository;
|
|||||||
import com.magistr.app.model.EducationForm;
|
import com.magistr.app.model.EducationForm;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface EducationFormRepository extends JpaRepository<EducationForm, Long> {
|
public interface EducationFormRepository extends JpaRepository<EducationForm, Long> {
|
||||||
|
|
||||||
Optional<EducationForm> findByName(String name);
|
Optional<EducationForm> findByName(String name);
|
||||||
|
|
||||||
|
List<EducationForm> findAllByOrderByNameAsc();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,17 +295,6 @@ ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
|
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS calendar_study_forms (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO calendar_study_forms (name, description) VALUES
|
|
||||||
('очная', 'Очная форма обучения')
|
|
||||||
ON CONFLICT (name) DO NOTHING;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
|
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
code VARCHAR(10) UNIQUE NOT NULL,
|
code VARCHAR(10) UNIQUE NOT NULL,
|
||||||
@@ -338,7 +327,7 @@ CREATE TABLE IF NOT EXISTS academic_calendars (
|
|||||||
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||||||
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
|
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
|
||||||
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
|
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
|
||||||
study_form_id BIGINT NOT NULL REFERENCES calendar_study_forms(id),
|
study_form_id BIGINT NOT NULL REFERENCES education_forms(id),
|
||||||
course_count INT NOT NULL DEFAULT 4,
|
course_count INT NOT NULL DEFAULT 4,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -358,7 +347,7 @@ SELECT
|
|||||||
study_form.id,
|
study_form.id,
|
||||||
4
|
4
|
||||||
FROM academic_years ay
|
FROM academic_years ay
|
||||||
CROSS JOIN calendar_study_forms study_form
|
CROSS JOIN education_forms study_form
|
||||||
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
|
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
|
||||||
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
|
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
|
||||||
WHERE ay.title = '2025-2026'
|
WHERE ay.title = '2025-2026'
|
||||||
@@ -569,7 +558,6 @@ COMMENT ON TABLE specialty_profiles IS 'Профили обучения внут
|
|||||||
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
|
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
|
||||||
COMMENT ON TABLE academic_years IS 'Учебные годы';
|
COMMENT ON TABLE academic_years IS 'Учебные годы';
|
||||||
COMMENT ON TABLE semesters IS 'Семестры учебного года';
|
COMMENT ON TABLE semesters IS 'Семестры учебного года';
|
||||||
COMMENT ON TABLE calendar_study_forms IS 'Формы обучения календарного учебного графика';
|
|
||||||
COMMENT ON TABLE academic_calendar_activity_types IS 'Коды активностей календарного учебного графика';
|
COMMENT ON TABLE academic_calendar_activity_types IS 'Коды активностей календарного учебного графика';
|
||||||
COMMENT ON TABLE academic_calendars IS 'Календарные учебные графики по профилю, форме обучения и учебному году';
|
COMMENT ON TABLE academic_calendars IS 'Календарные учебные графики по профилю, форме обучения и учебному году';
|
||||||
COMMENT ON TABLE academic_calendar_days IS 'Дневные ячейки календарного учебного графика';
|
COMMENT ON TABLE academic_calendar_days IS 'Дневные ячейки календарного учебного графика';
|
||||||
@@ -666,10 +654,6 @@ COMMENT ON COLUMN specialty_profiles.specialty_id IS 'ID специальнос
|
|||||||
COMMENT ON COLUMN specialty_profiles.name IS 'Название профиля обучения';
|
COMMENT ON COLUMN specialty_profiles.name IS 'Название профиля обучения';
|
||||||
COMMENT ON COLUMN specialty_profiles.description IS 'Описание профиля обучения';
|
COMMENT ON COLUMN specialty_profiles.description IS 'Описание профиля обучения';
|
||||||
|
|
||||||
COMMENT ON COLUMN calendar_study_forms.id IS 'ID формы обучения графика';
|
|
||||||
COMMENT ON COLUMN calendar_study_forms.name IS 'Название формы обучения графика';
|
|
||||||
COMMENT ON COLUMN calendar_study_forms.description IS 'Описание формы обучения графика';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN academic_calendar_activity_types.code IS 'Код активности из календарного графика';
|
COMMENT ON COLUMN academic_calendar_activity_types.code IS 'Код активности из календарного графика';
|
||||||
COMMENT ON COLUMN academic_calendar_activity_types.name IS 'Название активности';
|
COMMENT ON COLUMN academic_calendar_activity_types.name IS 'Название активности';
|
||||||
COMMENT ON COLUMN academic_calendar_activity_types.allow_schedule IS 'Разрешает ли код генерацию обычного расписания';
|
COMMENT ON COLUMN academic_calendar_activity_types.allow_schedule IS 'Разрешает ли код генерацию обычного расписания';
|
||||||
@@ -678,7 +662,7 @@ COMMENT ON COLUMN academic_calendars.title IS 'Название календар
|
|||||||
COMMENT ON COLUMN academic_calendars.academic_year_id IS 'ID учебного года';
|
COMMENT ON COLUMN academic_calendars.academic_year_id IS 'ID учебного года';
|
||||||
COMMENT ON COLUMN academic_calendars.specialty_id IS 'ID специальности';
|
COMMENT ON COLUMN academic_calendars.specialty_id IS 'ID специальности';
|
||||||
COMMENT ON COLUMN academic_calendars.specialty_profile_id IS 'ID профиля обучения';
|
COMMENT ON COLUMN academic_calendars.specialty_profile_id IS 'ID профиля обучения';
|
||||||
COMMENT ON COLUMN academic_calendars.study_form_id IS 'ID формы обучения графика';
|
COMMENT ON COLUMN academic_calendars.study_form_id IS 'ID формы обучения из education_forms';
|
||||||
COMMENT ON COLUMN academic_calendars.course_count IS 'Количество курсов в графике';
|
COMMENT ON COLUMN academic_calendars.course_count IS 'Количество курсов в графике';
|
||||||
|
|
||||||
COMMENT ON COLUMN academic_calendar_days.calendar_id IS 'ID календарного учебного графика';
|
COMMENT ON COLUMN academic_calendar_days.calendar_id IS 'ID календарного учебного графика';
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
- `specialties` хранит код и название специальности, например `09.03.04`.
|
- `specialties` хранит код и название специальности, например `09.03.04`.
|
||||||
- `specialty_profiles` хранит профили внутри специальности. У одной специальности может быть несколько профилей.
|
- `specialty_profiles` хранит профили внутри специальности. У одной специальности может быть несколько профилей.
|
||||||
- `academic_calendars` хранит график профиля, формы обучения и учебного года.
|
- `academic_calendars` хранит график профиля, формы обучения и учебного года.
|
||||||
|
- Форма обучения графика берётся из общего справочника `education_forms`; отдельной таблицы форм для календаря нет.
|
||||||
- `academic_calendar_days` хранит дневную сетку графика по курсу и дате.
|
- `academic_calendar_days` хранит дневную сетку графика по курсу и дате.
|
||||||
- `student_groups` связана со специальностью и профилем обучения.
|
- `student_groups` связана со специальностью, профилем и той же таблицей форм обучения.
|
||||||
- `student_group_calendar_assignments` назначает группе конкретный график на учебный год.
|
- `student_group_calendar_assignments` назначает группе конкретный график на учебный год.
|
||||||
|
|
||||||
Группа выбирает не код активности напрямую, а календарный график. Поэтому один график можно назначить нескольким группам.
|
Группа выбирает не код активности напрямую, а календарный график. Поэтому один график можно назначить нескольким группам.
|
||||||
@@ -60,8 +61,8 @@
|
|||||||
|
|
||||||
1. В разделе кафедр и специальностей администратор создаёт специальность и её профили.
|
1. В разделе кафедр и специальностей администратор создаёт специальность и её профили.
|
||||||
2. В разделе групп администратор создаёт группу, выбирая специальность и профиль обучения.
|
2. В разделе групп администратор создаёт группу, выбирая специальность и профиль обучения.
|
||||||
3. В разделе расписания администратор создаёт учебный год и семестры.
|
3. В разделе календарного графика администратор создаёт учебный год и семестры.
|
||||||
4. Там же создаётся календарный учебный график: название, учебный год, специальность, профиль, форма обучения и количество курсов.
|
4. Там же создаётся календарный учебный график: название, учебный год, специальность, профиль, форма обучения из общего справочника и количество курсов.
|
||||||
5. Администратор открывает сетку графика, заполняет дни вручную или массово через диапазон дат.
|
5. Администратор открывает сетку графика, заполняет дни вручную или массово через диапазон дат.
|
||||||
6. В разделе групп администратор назначает календарный график группе на нужный учебный год.
|
6. В разделе групп администратор назначает календарный график группе на нужный учебный год.
|
||||||
|
|
||||||
@@ -72,5 +73,6 @@
|
|||||||
- Старый отдельный механизм праздников не используется: праздник хранится кодом `*` в дневной сетке.
|
- Старый отдельный механизм праздников не используется: праздник хранится кодом `*` в дневной сетке.
|
||||||
- Дни вне учебного года или пустые периоды Excel хранятся кодом `=`.
|
- Дни вне учебного года или пустые периоды Excel хранятся кодом `=`.
|
||||||
- График привязан к профилю обучения, а не только к коду специальности.
|
- График привязан к профилю обучения, а не только к коду специальности.
|
||||||
|
- Форма обучения графика должна совпадать с формой обучения группы при назначении графика.
|
||||||
- Название группы не является уникальным ключом; идентичные названия допустимы, если различаются другие параметры.
|
- Название группы не является уникальным ключом; идентичные названия допустимы, если различаются другие параметры.
|
||||||
- Excel-импорт в первой реализации не предусмотрен: редактор заполняет данные через сайт.
|
- Excel-импорт в первой реализации не предусмотрен: редактор заполняет данные через сайт.
|
||||||
|
|||||||
@@ -212,7 +212,6 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
|
|||||||
| Метод | URL | Назначение |
|
| Метод | URL | Назначение |
|
||||||
|-------|-----|------------|
|
|-------|-----|------------|
|
||||||
| `GET` | `/api/admin/academic-calendars` | Список графиков, фильтры `academicYearId`, `specialtyId`, `profileId` |
|
| `GET` | `/api/admin/academic-calendars` | Список графиков, фильтры `academicYearId`, `specialtyId`, `profileId` |
|
||||||
| `GET` | `/api/admin/academic-calendars/study-forms` | Формы обучения графика |
|
|
||||||
| `GET` | `/api/admin/academic-calendars/{id}` | Один график |
|
| `GET` | `/api/admin/academic-calendars/{id}` | Один график |
|
||||||
| `POST` | `/api/admin/academic-calendars` | Создать график |
|
| `POST` | `/api/admin/academic-calendars` | Создать график |
|
||||||
| `PUT` | `/api/admin/academic-calendars/{id}` | Обновить график |
|
| `PUT` | `/api/admin/academic-calendars/{id}` | Обновить график |
|
||||||
@@ -232,6 +231,8 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`studyFormId` берётся из общего справочника форм обучения `GET /api/education-forms`; отдельного справочника форм для календарных графиков нет.
|
||||||
|
|
||||||
**Ячейка сетки графика:**
|
**Ячейка сетки графика:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -558,7 +559,7 @@ CRUD доступен по:
|
|||||||
|
|
||||||
### `DELETE /api/education-forms/{id}`
|
### `DELETE /api/education-forms/{id}`
|
||||||
|
|
||||||
Удаление формы обучения. **Невозможно**, если к ней привязаны группы.
|
Удаление формы обучения. **Невозможно**, если к ней привязаны группы или календарные учебные графики.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@
|
|||||||
|
|
||||||
### Формы обучения (Education Forms)
|
### Формы обучения (Education Forms)
|
||||||
|
|
||||||
Уровни/формы обучения для привязки к группам.
|
Уровни/формы обучения для привязки к группам и календарным учебным графикам.
|
||||||
|
|
||||||
- Предзаполнены: Бакалавриат, Магистратура, Специалитет
|
- Предзаполнены: Бакалавриат, Магистратура, Специалитет
|
||||||
- Нельзя удалить форму обучения, если к ней привязаны группы
|
- Нельзя удалить форму обучения, если к ней привязаны группы или календарные графики
|
||||||
|
|
||||||
### Учебные группы (Student Groups)
|
### Учебные группы (Student Groups)
|
||||||
|
|
||||||
|
|||||||
@@ -148,12 +148,6 @@ erDiagram
|
|||||||
DATE end_date
|
DATE end_date
|
||||||
}
|
}
|
||||||
|
|
||||||
calendar_study_forms {
|
|
||||||
BIGSERIAL id PK
|
|
||||||
VARCHAR name UK
|
|
||||||
TEXT description
|
|
||||||
}
|
|
||||||
|
|
||||||
academic_calendar_activity_types {
|
academic_calendar_activity_types {
|
||||||
BIGSERIAL id PK
|
BIGSERIAL id PK
|
||||||
VARCHAR code UK
|
VARCHAR code UK
|
||||||
@@ -244,7 +238,7 @@ erDiagram
|
|||||||
semesters ||--o{ schedule_rules : "semester_id"
|
semesters ||--o{ schedule_rules : "semester_id"
|
||||||
specialties ||--o{ academic_calendars : "specialty_id"
|
specialties ||--o{ academic_calendars : "specialty_id"
|
||||||
specialty_profiles ||--o{ academic_calendars : "specialty_profile_id"
|
specialty_profiles ||--o{ academic_calendars : "specialty_profile_id"
|
||||||
calendar_study_forms ||--o{ academic_calendars : "study_form_id"
|
education_forms ||--o{ academic_calendars : "study_form_id"
|
||||||
academic_calendars ||--o{ academic_calendar_days : "calendar_id"
|
academic_calendars ||--o{ academic_calendar_days : "calendar_id"
|
||||||
academic_calendars ||--o{ student_group_calendar_assignments : "calendar_id"
|
academic_calendars ||--o{ student_group_calendar_assignments : "calendar_id"
|
||||||
academic_calendar_activity_types ||--o{ academic_calendar_days : "activity_type_id"
|
academic_calendar_activity_types ||--o{ academic_calendar_days : "activity_type_id"
|
||||||
@@ -309,6 +303,8 @@ erDiagram
|
|||||||
| `name` | VARCHAR(100) UNIQUE | Название (Бакалавриат, Магистратура, Специалитет) |
|
| `name` | VARCHAR(100) UNIQUE | Название (Бакалавриат, Магистратура, Специалитет) |
|
||||||
| `description` | TEXT | Описание |
|
| `description` | TEXT | Описание |
|
||||||
|
|
||||||
|
Эта таблица используется и группами (`student_groups.education_form_id`), и календарными учебными графиками (`academic_calendars.study_form_id`). Отдельного справочника форм обучения для календарей нет.
|
||||||
|
|
||||||
#### `student_groups` — Учебные группы
|
#### `student_groups` — Учебные группы
|
||||||
| Колонка | Тип | Описание |
|
| Колонка | Тип | Описание |
|
||||||
|---------|-----|----------|
|
|---------|-----|----------|
|
||||||
@@ -420,13 +416,6 @@ erDiagram
|
|||||||
| `start_date` | DATE | Дата начала, от неё считается неделя 1 |
|
| `start_date` | DATE | Дата начала, от неё считается неделя 1 |
|
||||||
| `end_date` | DATE | Дата окончания |
|
| `end_date` | DATE | Дата окончания |
|
||||||
|
|
||||||
#### `calendar_study_forms` — Формы обучения календарного графика
|
|
||||||
| Колонка | Тип | Описание |
|
|
||||||
|---------|-----|----------|
|
|
||||||
| `id` | BIGSERIAL PK | ID |
|
|
||||||
| `name` | VARCHAR(100) UNIQUE | Форма обучения графика, seed: `очная` |
|
|
||||||
| `description` | TEXT | Описание |
|
|
||||||
|
|
||||||
#### `academic_calendar_activity_types` — Коды активностей графика
|
#### `academic_calendar_activity_types` — Коды активностей графика
|
||||||
| Колонка | Тип | Описание |
|
| Колонка | Тип | Описание |
|
||||||
|---------|-----|----------|
|
|---------|-----|----------|
|
||||||
@@ -446,7 +435,7 @@ erDiagram
|
|||||||
| `academic_year_id` | BIGINT FK → academic_years | Учебный год |
|
| `academic_year_id` | BIGINT FK → academic_years | Учебный год |
|
||||||
| `specialty_id` | BIGINT FK → specialties | Специальность |
|
| `specialty_id` | BIGINT FK → specialties | Специальность |
|
||||||
| `specialty_profile_id` | BIGINT FK → specialty_profiles | Профиль обучения |
|
| `specialty_profile_id` | BIGINT FK → specialty_profiles | Профиль обучения |
|
||||||
| `study_form_id` | BIGINT FK → calendar_study_forms | Форма обучения графика |
|
| `study_form_id` | BIGINT FK → education_forms | Форма обучения из общего справочника |
|
||||||
| `course_count` | INT CHECK(> 0) | Количество курсов в сетке |
|
| `course_count` | INT CHECK(> 0) | Количество курсов в сетке |
|
||||||
| `created_at` | TIMESTAMP | Дата создания |
|
| `created_at` | TIMESTAMP | Дата создания |
|
||||||
| `updated_at` | TIMESTAMP | Дата обновления |
|
| `updated_at` | TIMESTAMP | Дата обновления |
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ frontend/
|
|||||||
│ │ ├── subjects.js # Управление дисциплинами
|
│ │ ├── subjects.js # Управление дисциплинами
|
||||||
│ │ ├── equipments.js # Управление оборудованием
|
│ │ ├── equipments.js # Управление оборудованием
|
||||||
│ │ ├── edu-forms.js # Формы обучения
|
│ │ ├── edu-forms.js # Формы обучения
|
||||||
│ │ ├── schedule.js # Конструктор правил, сетки пар и учебного графика
|
│ │ ├── profiles.js # Профили обучения специальностей
|
||||||
|
│ │ ├── schedule.js # Конструктор правил расписания и сетки пар
|
||||||
|
│ │ ├── academic-calendar.js # Календарные учебные графики
|
||||||
│ │ ├── database.js # Управление тенантами
|
│ │ ├── database.js # Управление тенантами
|
||||||
│ │ └── departments-data.js # Создание кафедры/специальности
|
│ │ └── departments-data.js # Создание кафедры/специальности
|
||||||
│ ├── views/ # HTML-шаблоны представлений
|
│ ├── views/ # HTML-шаблоны представлений
|
||||||
@@ -52,7 +54,9 @@ frontend/
|
|||||||
│ │ ├── subjects.html
|
│ │ ├── subjects.html
|
||||||
│ │ ├── equipments.html
|
│ │ ├── equipments.html
|
||||||
│ │ ├── edu-forms.html
|
│ │ ├── edu-forms.html
|
||||||
|
│ │ ├── profiles.html
|
||||||
│ │ ├── schedule.html
|
│ │ ├── schedule.html
|
||||||
|
│ │ ├── academic-calendar.html
|
||||||
│ │ ├── database.html
|
│ │ ├── database.html
|
||||||
│ │ └── departments-data.html
|
│ │ └── departments-data.html
|
||||||
│ │
|
│ │
|
||||||
@@ -85,6 +89,7 @@ frontend/
|
|||||||
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
|
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
|
||||||
<a href="#" class="nav-item" data-tab="groups">Группы</a>
|
<a href="#" class="nav-item" data-tab="groups">Группы</a>
|
||||||
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
|
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
|
||||||
|
<a href="#" class="nav-item" data-tab="academic-calendar">Календарный график</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
При клике на пункт меню `main.js`:
|
При клике на пункт меню `main.js`:
|
||||||
@@ -100,10 +105,12 @@ frontend/
|
|||||||
| `users` | CRUD пользователей | `/api/users` |
|
| `users` | CRUD пользователей | `/api/users` |
|
||||||
| `groups` | CRUD групп | `/api/groups` |
|
| `groups` | CRUD групп | `/api/groups` |
|
||||||
| `edu-forms` | Формы обучения | `/api/education-forms` |
|
| `edu-forms` | Формы обучения | `/api/education-forms` |
|
||||||
|
| `profiles` | Создание, редактирование и удаление профилей обучения специальностей | `/api/specialties/{id}/profiles` |
|
||||||
| `equipments` | Оборудование | `/api/equipments` |
|
| `equipments` | Оборудование | `/api/equipments` |
|
||||||
| `classrooms` | Аудитории | `/api/classrooms` |
|
| `classrooms` | Аудитории | `/api/classrooms` |
|
||||||
| `subjects` | Дисциплины | `/api/subjects` |
|
| `subjects` | Дисциплины | `/api/subjects` |
|
||||||
| `schedule` | Конструктор правил динамического расписания, сетка пар, учебные годы, семестры и Excel-подобный редактор календарного учебного графика | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/lesson-types` |
|
| `schedule` | Конструктор правил динамического расписания и сетка пар | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types` |
|
||||||
|
| `academic-calendar` | Учебные годы, семестры, создание календарных графиков и Excel-подобный редактор дневной сетки | `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/admin/calendar/activity-types`, `/api/education-forms` |
|
||||||
| `auditorium-workload` | Загруженность аудиторий: аудитории в строках, временные слоты в столбцах | mock-данные |
|
| `auditorium-workload` | Загруженность аудиторий: аудитории в строках, временные слоты в столбцах | mock-данные |
|
||||||
| `database` | Тенанты | `/api/database` |
|
| `database` | Тенанты | `/api/database` |
|
||||||
| `departments-data` | Создание, редактирование и удаление кафедр/специальностей | `/api/departments`, `/api/specialties` |
|
| `departments-data` | Создание, редактирование и удаление кафедр/специальностей | `/api/departments`, `/api/specialties` |
|
||||||
@@ -112,8 +119,11 @@ frontend/
|
|||||||
|
|
||||||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, а блок назначений использует `/api/groups/{id}/calendar-assignments`.
|
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, а блок назначений использует `/api/groups/{id}/calendar-assignments`.
|
||||||
- Вкладка `auditorium-workload` показывает матрицу загруженности: строки — аудитории, столбцы — временные слоты.
|
- Вкладка `auditorium-workload` показывает матрицу загруженности: строки — аудитории, столбцы — временные слоты.
|
||||||
- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы и набор слотов. В этом же модуле администратор создаёт календарные графики, заполняет дневную сетку по курсам и кодам активностей, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`.
|
- Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили.
|
||||||
- Вкладка `departments-data` использует модальные формы редактирования и маршруты `PUT/DELETE /api/departments/{id}`, `PUT/DELETE /api/specialties/{id}` и CRUD профилей `/api/specialties/{id}/profiles`.
|
- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы и набор слотов. Из календарной системы здесь используется только список семестров для выбора периода действия правила.
|
||||||
|
- Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий: администратор создаёт учебные годы и семестры, заводит календарные графики, выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`.
|
||||||
|
- Редактор годового графика во вкладке `academic-calendar` показывает мелкую табличную сетку: каждый курс разделён на семестры (`1`/`2`, `3`/`4` и далее), внутри семестра столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. Подробная расшифровка и изменение кода открываются в модальном окне по клику на ячейку.
|
||||||
|
- Вкладка `departments-data` использует модальные формы редактирования и маршруты `PUT/DELETE /api/departments/{id}` и `PUT/DELETE /api/specialties/{id}`.
|
||||||
|
|
||||||
### Страница настроек (`/admin/settings/`)
|
### Страница настроек (`/admin/settings/`)
|
||||||
|
|
||||||
|
|||||||
@@ -199,40 +199,138 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-course-header span,
|
.calendar-course-header span {
|
||||||
.calendar-week-label {
|
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-days-grid {
|
.calendar-semesters-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
|
gap: 0.75rem;
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-day {
|
.calendar-semester-block {
|
||||||
display: grid;
|
|
||||||
gap: 0.4rem;
|
|
||||||
min-height: 92px;
|
|
||||||
padding: 0.55rem;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--bg-card-border);
|
border: 1px solid var(--bg-card-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-card);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-day-meta {
|
.calendar-semester-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.35rem;
|
gap: 1rem;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
border-bottom: 1px solid var(--bg-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-header span {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.78rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 600;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-table {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-table th,
|
||||||
|
.calendar-semester-table td {
|
||||||
|
border: 1px solid var(--bg-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
height: 22px;
|
||||||
|
min-width: 26px;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-semester-table .calendar-day-head {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
width: 36px;
|
||||||
|
min-width: 36px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
position: relative;
|
||||||
|
width: 26px;
|
||||||
|
min-width: 26px;
|
||||||
|
height: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
background: color-mix(in srgb, var(--activity-color, var(--accent)) 9%, var(--bg-card));
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 2px 0 var(--activity-color, var(--accent));
|
||||||
|
text-align: center;
|
||||||
|
transition: background var(--transition), box-shadow var(--transition), transform var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover,
|
||||||
|
.calendar-day:focus-visible {
|
||||||
|
background: color-mix(in srgb, var(--activity-color, var(--accent)) 18%, var(--bg-card));
|
||||||
|
box-shadow: inset 0 2px 0 var(--activity-color, var(--accent)), 0 0 0 2px color-mix(in srgb, var(--activity-color, var(--accent)) 36%, transparent);
|
||||||
|
transform: scale(1.08);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-code {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-day-activity {
|
.calendar-day-activity {
|
||||||
min-width: 0;
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-empty-day {
|
||||||
|
width: 26px;
|
||||||
|
min-width: 26px;
|
||||||
|
height: 24px;
|
||||||
|
background: color-mix(in srgb, var(--bg-input) 65%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-empty-semester {
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-totals {
|
.calendar-totals {
|
||||||
@@ -254,6 +352,71 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-card {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-grid div {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--bg-card-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-input);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-grid span {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-grid strong {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-activity {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.8rem 0 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--bg-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-code {
|
||||||
|
width: 46px;
|
||||||
|
min-width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--activity-color, var(--accent)) 20%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--activity-color, var(--accent)) 55%, transparent);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-activity p {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiles-context {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: -0.25rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -266,6 +429,12 @@
|
|||||||
.calendar-fill-panel {
|
.calendar-fill-panel {
|
||||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-semester-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -276,6 +445,10 @@
|
|||||||
.calendar-fill-panel {
|
.calendar-fill-panel {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-day-dialog-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide Number Arrows */
|
/* Hide Number Arrows */
|
||||||
|
|||||||
@@ -60,6 +60,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Создание кафедры/специальности</span>
|
<span>Создание кафедры/специальности</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" class="nav-item" data-tab="profiles">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||||
|
<circle cx="12" cy="7" r="4"></circle>
|
||||||
|
<path d="M16 11.5 18 14l3-5"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Профили обучения</span>
|
||||||
|
</a>
|
||||||
<a href="#" class="nav-item" data-tab="groups">
|
<a href="#" class="nav-item" data-tab="groups">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -110,6 +118,16 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Расписание занятий</span>
|
<span>Расписание занятий</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" class="nav-item" data-tab="academic-calendar">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3h18v18H3z"></path>
|
||||||
|
<path d="M3 9h18"></path>
|
||||||
|
<path d="M9 3v18"></path>
|
||||||
|
<path d="M15 3v18"></path>
|
||||||
|
<path d="M3 15h18"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Календарный график</span>
|
||||||
|
</a>
|
||||||
<a href="#" class="nav-item" data-tab="auditorium-workload">
|
<a href="#" class="nav-item" data-tab="auditorium-workload">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import {initSchedule} from "./views/schedule.js";
|
|||||||
import {initDatabase} from "./views/database.js";
|
import {initDatabase} from "./views/database.js";
|
||||||
import {initDepartmentsData} from "./views/departments-data.js";
|
import {initDepartmentsData} from "./views/departments-data.js";
|
||||||
import {initAuditoriumWorkload} from "./views/auditorium-workload.js";
|
import {initAuditoriumWorkload} from "./views/auditorium-workload.js";
|
||||||
|
import {initProfiles} from "./views/profiles.js";
|
||||||
|
import {initAcademicCalendar} from "./views/academic-calendar.js";
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
@@ -38,9 +40,11 @@ const ROUTES = {
|
|||||||
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
||||||
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
||||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||||
|
'academic-calendar': { title: 'Календарный учебный график', file: 'views/academic-calendar.html', init: initAcademicCalendar },
|
||||||
'auditorium-workload': { title: 'Загруженность аудиторий', file: 'views/auditorium-workload.html', init: initAuditoriumWorkload },
|
'auditorium-workload': { title: 'Загруженность аудиторий', file: 'views/auditorium-workload.html', init: initAuditoriumWorkload },
|
||||||
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
||||||
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
||||||
|
profiles: { title: 'Профили обучения', file: 'views/profiles.html', init: initProfiles },
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentTab = null;
|
let currentTab = null;
|
||||||
|
|||||||
869
frontend/admin/js/views/academic-calendar.js
Normal file
869
frontend/admin/js/views/academic-calendar.js
Normal file
@@ -0,0 +1,869 @@
|
|||||||
|
import { api } from '../api.js';
|
||||||
|
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||||
|
|
||||||
|
const DAY_OPTIONS = [
|
||||||
|
{ value: 1, label: 'Понедельник' },
|
||||||
|
{ value: 2, label: 'Вторник' },
|
||||||
|
{ value: 3, label: 'Среда' },
|
||||||
|
{ value: 4, label: 'Четверг' },
|
||||||
|
{ value: 5, label: 'Пятница' },
|
||||||
|
{ value: 6, label: 'Суббота' },
|
||||||
|
{ value: 7, label: 'Воскресенье' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SEMESTER_LABELS = {
|
||||||
|
autumn: 'осенний',
|
||||||
|
spring: 'весенний'
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function initAcademicCalendar() {
|
||||||
|
const academicYearForm = document.getElementById('academic-year-form');
|
||||||
|
const academicYearFormTitle = document.getElementById('academic-year-form-title');
|
||||||
|
const academicYearIdInput = document.getElementById('academic-year-id');
|
||||||
|
const academicYearTitleInput = document.getElementById('academic-year-title');
|
||||||
|
const academicYearStartInput = document.getElementById('academic-year-start');
|
||||||
|
const academicYearEndInput = document.getElementById('academic-year-end');
|
||||||
|
const academicYearResetButton = document.getElementById('academic-year-reset');
|
||||||
|
const academicYearsTbody = document.getElementById('academic-years-tbody');
|
||||||
|
const academicYearsRefreshButton = document.getElementById('academic-years-refresh');
|
||||||
|
|
||||||
|
const semesterForm = document.getElementById('semester-form');
|
||||||
|
const semesterFormTitle = document.getElementById('semester-form-title');
|
||||||
|
const semesterIdInput = document.getElementById('semester-id');
|
||||||
|
const semesterYearSelect = document.getElementById('semester-year');
|
||||||
|
const semesterTypeSelect = document.getElementById('semester-type');
|
||||||
|
const semesterStartInput = document.getElementById('semester-start');
|
||||||
|
const semesterEndInput = document.getElementById('semester-end');
|
||||||
|
const semesterResetButton = document.getElementById('semester-reset');
|
||||||
|
|
||||||
|
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 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');
|
||||||
|
const calendarDayDialog = document.getElementById('calendar-day-dialog');
|
||||||
|
const calendarDayDialogClose = document.getElementById('calendar-day-dialog-close');
|
||||||
|
const calendarDayDialogDate = document.getElementById('calendar-day-dialog-date');
|
||||||
|
const calendarDayDialogCourse = document.getElementById('calendar-day-dialog-course');
|
||||||
|
const calendarDayDialogWeek = document.getElementById('calendar-day-dialog-week');
|
||||||
|
const calendarDayDialogDay = document.getElementById('calendar-day-dialog-day');
|
||||||
|
const calendarDayDialogActivitySummary = document.getElementById('calendar-day-dialog-activity-summary');
|
||||||
|
const calendarDayDialogActivitySelect = document.getElementById('calendar-day-dialog-activity');
|
||||||
|
|
||||||
|
let academicYears = [];
|
||||||
|
let semesters = [];
|
||||||
|
let specialties = [];
|
||||||
|
let profilesBySpecialty = new Map();
|
||||||
|
let studyForms = [];
|
||||||
|
let activityTypes = [];
|
||||||
|
let calendars = [];
|
||||||
|
let activeCalendarDay = null;
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([loadBaseLists(), loadYears(), loadStudyForms(), loadActivityTypes()]);
|
||||||
|
await loadCalendars();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('academic-calendar-alert', error.message || 'Ошибка загрузки календарных графиков', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
academicYearsRefreshButton?.addEventListener('click', loadYears);
|
||||||
|
calendarsRefreshButton?.addEventListener('click', loadCalendars);
|
||||||
|
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
|
||||||
|
semesterResetButton?.addEventListener('click', resetSemesterForm);
|
||||||
|
calendarResetButton?.addEventListener('click', resetCalendarForm);
|
||||||
|
calendarSpecialtySelect?.addEventListener('change', () => populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value));
|
||||||
|
editorLoadButton?.addEventListener('click', loadCalendarGrid);
|
||||||
|
editorSaveButton?.addEventListener('click', saveCalendarGrid);
|
||||||
|
fillApplyButton?.addEventListener('click', applyCalendarFill);
|
||||||
|
|
||||||
|
calendarGrid?.addEventListener('change', (event) => {
|
||||||
|
if (event.target.matches('.calendar-day-activity')) {
|
||||||
|
updateCalendarDayVisual(event.target.closest('.calendar-day'));
|
||||||
|
renderCalendarTotals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendarGrid?.addEventListener('click', (event) => {
|
||||||
|
const day = event.target.closest('.calendar-day');
|
||||||
|
if (day) openCalendarDayDialog(day);
|
||||||
|
});
|
||||||
|
calendarGrid?.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||||
|
const day = event.target.closest('.calendar-day');
|
||||||
|
if (!day) return;
|
||||||
|
event.preventDefault();
|
||||||
|
openCalendarDayDialog(day);
|
||||||
|
});
|
||||||
|
calendarDayDialogClose?.addEventListener('click', closeCalendarDayDialog);
|
||||||
|
calendarDayDialog?.addEventListener('click', (event) => {
|
||||||
|
if (event.target === calendarDayDialog) closeCalendarDayDialog();
|
||||||
|
});
|
||||||
|
calendarDayDialogActivitySelect?.addEventListener('change', () => {
|
||||||
|
if (!activeCalendarDay) return;
|
||||||
|
const select = activeCalendarDay.querySelector('.calendar-day-activity');
|
||||||
|
select.value = calendarDayDialogActivitySelect.value;
|
||||||
|
updateCalendarDayVisual(activeCalendarDay);
|
||||||
|
updateCalendarDayDialogSummary(activeCalendarDay);
|
||||||
|
renderCalendarTotals();
|
||||||
|
});
|
||||||
|
|
||||||
|
academicYearForm?.addEventListener('submit', saveAcademicYear);
|
||||||
|
semesterForm?.addEventListener('submit', saveSemester);
|
||||||
|
calendarForm?.addEventListener('submit', saveCalendar);
|
||||||
|
|
||||||
|
academicYearsTbody?.addEventListener('click', handleAcademicYearTableClick);
|
||||||
|
calendarsTbody?.addEventListener('click', handleCalendarTableClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBaseLists() {
|
||||||
|
specialties = await api.get('/api/specialties');
|
||||||
|
await loadProfiles();
|
||||||
|
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 loadYears() {
|
||||||
|
academicYearsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
|
||||||
|
try {
|
||||||
|
academicYears = await api.get('/api/admin/calendar/years');
|
||||||
|
semesters = academicYears.flatMap(year => year.semesters || []);
|
||||||
|
renderAcademicYears();
|
||||||
|
populateYearSelects();
|
||||||
|
} catch (error) {
|
||||||
|
academicYearsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAcademicYears() {
|
||||||
|
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>${renderSemesterList(year.semesters || [])}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button>
|
||||||
|
<button class="btn-delete btn-delete-year" data-id="${year.id}">Удалить</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSemesterList(items) {
|
||||||
|
if (!items.length) return '<span class="muted">Нет семестров</span>';
|
||||||
|
return items.map(semester => `
|
||||||
|
<div class="schedule-semester-line">
|
||||||
|
${escapeHtml(semesterLabel(semester))}
|
||||||
|
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}">Изменить</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
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('Заполните название и даты учебного года');
|
||||||
|
const id = academicYearIdInput.value;
|
||||||
|
await (id
|
||||||
|
? api.put('/api/admin/calendar/years/' + id, payload)
|
||||||
|
: api.post('/api/admin/calendar/years', payload));
|
||||||
|
showAlert('academic-year-alert', 'Учебный год сохранён', 'success');
|
||||||
|
resetAcademicYearForm();
|
||||||
|
await loadYears();
|
||||||
|
await loadCalendars();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAcademicYearTableClick(event) {
|
||||||
|
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;
|
||||||
|
academicYearFormTitle.textContent = 'Редактирование учебного года';
|
||||||
|
academicYearIdInput.value = year.id;
|
||||||
|
academicYearTitleInput.value = year.title || '';
|
||||||
|
academicYearStartInput.value = year.startDate || '';
|
||||||
|
academicYearEndInput.value = year.endDate || '';
|
||||||
|
hideAlert('academic-year-alert');
|
||||||
|
academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editSemesterButton) {
|
||||||
|
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
|
||||||
|
if (!semester) return;
|
||||||
|
semesterFormTitle.textContent = 'Редактирование семестра';
|
||||||
|
semesterIdInput.value = semester.id;
|
||||||
|
semesterYearSelect.value = semester.academicYearId || '';
|
||||||
|
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
|
||||||
|
semesterStartInput.value = semester.startDate || '';
|
||||||
|
semesterEndInput.value = semester.endDate || '';
|
||||||
|
syncSelects(semesterYearSelect, semesterTypeSelect);
|
||||||
|
hideAlert('semester-alert');
|
||||||
|
semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteYearButton) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAcademicYearForm() {
|
||||||
|
academicYearForm.reset();
|
||||||
|
academicYearFormTitle.textContent = 'Учебный год';
|
||||||
|
academicYearIdInput.value = '';
|
||||||
|
hideAlert('academic-year-alert');
|
||||||
|
}
|
||||||
|
|
||||||
|
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('Заполните учебный год, тип и даты семестра');
|
||||||
|
}
|
||||||
|
const id = semesterIdInput.value;
|
||||||
|
await (id
|
||||||
|
? api.put('/api/admin/calendar/semesters/' + id, payload)
|
||||||
|
: api.post(`/api/admin/calendar/years/${yearId}/semesters`, payload));
|
||||||
|
showAlert('semester-alert', 'Семестр сохранён', 'success');
|
||||||
|
resetSemesterForm();
|
||||||
|
await loadYears();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('semester-alert', error.message || 'Ошибка сохранения семестра', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSemesterForm() {
|
||||||
|
semesterForm.reset();
|
||||||
|
semesterFormTitle.textContent = 'Семестр';
|
||||||
|
semesterIdInput.value = '';
|
||||||
|
syncSelects(semesterYearSelect, semesterTypeSelect);
|
||||||
|
hideAlert('semester-alert');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStudyForms() {
|
||||||
|
studyForms = await api.get('/api/education-forms');
|
||||||
|
populateStudyFormSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
calendars = await api.get('/api/admin/academic-calendars');
|
||||||
|
renderCalendars();
|
||||||
|
populateEditorCalendarSelect();
|
||||||
|
} catch (error) {
|
||||||
|
calendarsTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCalendars() {
|
||||||
|
if (!calendars.length) {
|
||||||
|
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Календарные графики не созданы</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calendarsTbody.innerHTML = calendars.map(calendar => `
|
||||||
|
<tr>
|
||||||
|
<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-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 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 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;
|
||||||
|
try {
|
||||||
|
await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
|
||||||
|
showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
|
||||||
|
await loadCalendars();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('academic-calendar-alert', error.message || 'Ошибка удаления календарного графика', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 loadCalendarGrid() {
|
||||||
|
hideAlert('calendar-editor-alert');
|
||||||
|
const calendar = selectedEditorCalendar();
|
||||||
|
if (!calendar) {
|
||||||
|
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
|
||||||
|
const gridRows = rows.length ? rows : buildDefaultGrid(calendar);
|
||||||
|
renderCalendarGrid(gridRows);
|
||||||
|
showAlert('calendar-editor-alert', rows.length ? 'Сетка загружена' : 'Подготовлена новая сетка по датам учебного года', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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('calendar-editor-alert', 'Сначала загрузите сетку графика', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.put(`/api/admin/academic-calendars/${calendar.id}/grid`, rows);
|
||||||
|
showAlert('calendar-editor-alert', 'Календарный учебный график сохранён', 'success');
|
||||||
|
await loadCalendarGrid();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('calendar-editor-alert', error.message || 'Ошибка сохранения сетки', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
updateCalendarDayVisual(day);
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderCalendarTotals();
|
||||||
|
showAlert('calendar-editor-alert', `Обновлено ячеек: ${changed}`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
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-semesters-grid">
|
||||||
|
${renderCourseSemesters(Number(course), courseRows)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`).join('');
|
||||||
|
populateFillCourseSelect();
|
||||||
|
renderCalendarTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCourseSemesters(course, courseRows) {
|
||||||
|
const calendar = selectedEditorCalendar();
|
||||||
|
const semestersForCourse = semesterBlocksForCourse(calendar, course, courseRows);
|
||||||
|
return semestersForCourse.map(block => `
|
||||||
|
<section class="calendar-semester-block">
|
||||||
|
<div class="calendar-semester-header">
|
||||||
|
<h4>${block.semesterNumber} семестр</h4>
|
||||||
|
<span>${escapeHtml(block.caption)}</span>
|
||||||
|
</div>
|
||||||
|
${renderSemesterTable(block.rows)}
|
||||||
|
</section>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSemesterTable(rows) {
|
||||||
|
if (!rows.length) {
|
||||||
|
return '<div class="calendar-empty-semester">Нет дат в этом семестре</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsByWeek = groupBy(rows, row => row.weekNumber);
|
||||||
|
const weeks = Array.from(rowsByWeek.keys()).sort((a, b) => Number(a) - Number(b));
|
||||||
|
const cellByWeekAndDay = new Map(rows.map(row => [`${row.weekNumber}:${row.dayOfWeek}`, row]));
|
||||||
|
return `
|
||||||
|
<div class="calendar-semester-table-wrap">
|
||||||
|
<table class="calendar-semester-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="calendar-day-head">День</th>
|
||||||
|
${weeks.map(week => `<th title="Неделя ${week} учебного года">${week}</th>`).join('')}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${DAY_OPTIONS.map(day => `
|
||||||
|
<tr>
|
||||||
|
<th class="calendar-day-head">${escapeHtml(day.label.slice(0, 2))}</th>
|
||||||
|
${weeks.map(week => {
|
||||||
|
const row = cellByWeekAndDay.get(`${week}:${day.value}`);
|
||||||
|
return row ? renderCalendarDay(row) : '<td class="calendar-empty-day"></td>';
|
||||||
|
}).join('')}
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCalendarDay(row) {
|
||||||
|
const selected = row.activityTypeId || activityIdByCode(row.activityCode) || theoryActivityId();
|
||||||
|
const dayName = shortDayLabel(row.dayOfWeek);
|
||||||
|
const activity = activityById(selected) || activityTypes[0] || {};
|
||||||
|
const code = activity.code || row.activityCode || 'Т';
|
||||||
|
const color = activity.colorCode || '#64748b';
|
||||||
|
return `
|
||||||
|
<td class="calendar-day"
|
||||||
|
data-course="${row.courseNumber}"
|
||||||
|
data-date="${row.date}"
|
||||||
|
data-week="${row.weekNumber}"
|
||||||
|
data-day="${row.dayOfWeek}"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="${escapeHtml(`${formatShortDate(row.date)} ${dayName}, ${row.weekNumber} неделя, код ${code}`)}"
|
||||||
|
title="${escapeHtml(`${formatLongDate(row.date)}, ${dayName}, ${row.weekNumber} неделя, ${code} - ${activity.name || ''}`)}"
|
||||||
|
style="--activity-color:${escapeHtml(color)}">
|
||||||
|
<strong class="calendar-day-code">${escapeHtml(code)}</strong>
|
||||||
|
<select class="calendar-day-activity custom-select-initialized" aria-hidden="true" tabindex="-1">${options(activityTypes.map(toActivityOption), selected)}</select>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCalendarDayDialog(day) {
|
||||||
|
activeCalendarDay = day;
|
||||||
|
calendarDayDialogDate.textContent = `${formatLongDate(day.dataset.date)} (${shortDayLabel(day.dataset.day)})`;
|
||||||
|
calendarDayDialogCourse.textContent = `${day.dataset.course} курс`;
|
||||||
|
calendarDayDialogWeek.textContent = `${day.dataset.week} неделя`;
|
||||||
|
calendarDayDialogDay.textContent = dayLabel(day.dataset.day);
|
||||||
|
calendarDayDialogActivitySelect.innerHTML = options(
|
||||||
|
activityTypes.map(toActivityOption),
|
||||||
|
day.querySelector('.calendar-day-activity').value
|
||||||
|
);
|
||||||
|
syncSelects(calendarDayDialogActivitySelect);
|
||||||
|
updateCalendarDayDialogSummary(day);
|
||||||
|
calendarDayDialog.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCalendarDayDialog() {
|
||||||
|
activeCalendarDay = null;
|
||||||
|
calendarDayDialog.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalendarDayDialogSummary(day) {
|
||||||
|
const activity = activityById(day.querySelector('.calendar-day-activity').value);
|
||||||
|
if (!activity) {
|
||||||
|
calendarDayDialogActivitySummary.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calendarDayDialogActivitySummary.innerHTML = `
|
||||||
|
<span class="calendar-day-dialog-code" style="--activity-color:${escapeHtml(activity.colorCode || '#64748b')}">${escapeHtml(activity.code)}</span>
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(activity.name)}</strong>
|
||||||
|
<p>${escapeHtml(activity.description || (activity.allowSchedule ? 'Обычные пары разрешены' : 'Обычные пары не генерируются'))}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalendarDayVisual(day) {
|
||||||
|
if (!day) return;
|
||||||
|
const select = day.querySelector('.calendar-day-activity');
|
||||||
|
const activity = activityById(select.value);
|
||||||
|
const code = activity?.code || '-';
|
||||||
|
const color = activity?.colorCode || '#64748b';
|
||||||
|
day.style.setProperty('--activity-color', color);
|
||||||
|
day.querySelector('.calendar-day-code').textContent = code;
|
||||||
|
day.setAttribute('aria-label', `${formatShortDate(day.dataset.date)} ${shortDayLabel(day.dataset.day)}, ${day.dataset.week} неделя, код ${code}`);
|
||||||
|
day.setAttribute('title', `${formatLongDate(day.dataset.date)}, ${shortDayLabel(day.dataset.day)}, ${day.dataset.week} неделя, ${code} - ${activity?.name || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 semesterBlocksForCourse(calendar, course, courseRows) {
|
||||||
|
const sortedRows = [...courseRows].sort((a, b) => String(a.date).localeCompare(String(b.date)));
|
||||||
|
const year = academicYears.find(item => String(item.id) === String(calendar?.academicYearId));
|
||||||
|
const yearSemesters = [...(year?.semesters || [])]
|
||||||
|
.sort((a, b) => String(a.startDate).localeCompare(String(b.startDate)));
|
||||||
|
|
||||||
|
if (!yearSemesters.length) {
|
||||||
|
return [{
|
||||||
|
semesterNumber: `${course * 2 - 1}-${course * 2}`,
|
||||||
|
caption: dateRangeCaption(sortedRows),
|
||||||
|
rows: sortedRows
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return yearSemesters.map((semester, index) => {
|
||||||
|
const nextSemester = yearSemesters[index + 1];
|
||||||
|
const startDate = index === 0
|
||||||
|
? minIso(calendar.academicYearStartDate, semester.startDate)
|
||||||
|
: semester.startDate;
|
||||||
|
const endDate = nextSemester
|
||||||
|
? previousIsoDate(nextSemester.startDate)
|
||||||
|
: maxIso(calendar.academicYearEndDate, semester.endDate);
|
||||||
|
const rows = sortedRows.filter(row => row.date >= startDate && row.date <= endDate);
|
||||||
|
return {
|
||||||
|
semesterNumber: course * 2 - 1 + index,
|
||||||
|
caption: dateRangeCaption(rows) || `${formatLongDate(startDate)} - ${formatLongDate(endDate)}`,
|
||||||
|
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 populateYearSelects() {
|
||||||
|
const yearOptions = '<option value="">Выберите учебный год</option>' +
|
||||||
|
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
||||||
|
semesterYearSelect.innerHTML = yearOptions;
|
||||||
|
calendarYearSelect.innerHTML = yearOptions;
|
||||||
|
syncSelects(semesterYearSelect, calendarYearSelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateSpecialtySelects() {
|
||||||
|
calendarSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||||
|
specialties.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality))}</option>`).join('');
|
||||||
|
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) {
|
||||||
|
const selected = selectedValue == null ? '' : String(selectedValue);
|
||||||
|
return '<option value="">Выберите</option>' + items.map(item => {
|
||||||
|
const value = String(item.value);
|
||||||
|
return `<option value="${escapeHtml(value)}" ${value === selected ? 'selected' : ''}>${escapeHtml(item.label)}</option>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toActivityOption(type) {
|
||||||
|
return { value: type.id, label: `${type.code} - ${type.name}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
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})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSemesterType(type) {
|
||||||
|
return String(type || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayLabel(value) {
|
||||||
|
return DAY_OPTIONS.find(day => String(day.value) === String(value))?.label || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortDayLabel(value) {
|
||||||
|
const label = dayLabel(value);
|
||||||
|
return label === '-' ? '-' : label.slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatShortDate(value) {
|
||||||
|
const [year, month, day] = String(value).split('-');
|
||||||
|
return `${day}.${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLongDate(value) {
|
||||||
|
const [year, month, day] = String(value).split('-');
|
||||||
|
return `${day}.${month}.${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateRangeCaption(rows) {
|
||||||
|
if (!rows.length) return '';
|
||||||
|
const dates = rows.map(row => row.date).sort();
|
||||||
|
return `${formatLongDate(dates[0])} - ${formatLongDate(dates[dates.length - 1])}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function minIso(a, b) {
|
||||||
|
return String(a) <= String(b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxIso(a, b) {
|
||||||
|
return String(a) >= String(b) ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousIsoDate(value) {
|
||||||
|
const date = new Date(`${value}T00:00:00Z`);
|
||||||
|
date.setUTCDate(date.getUTCDate() - 1);
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,22 +7,15 @@ export async function initDepartmentsData() {
|
|||||||
|
|
||||||
const createDeptForm = document.getElementById('create-department-form');
|
const createDeptForm = document.getElementById('create-department-form');
|
||||||
const createSpecForm = document.getElementById('create-specialty-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 editDeptForm = document.getElementById('edit-department-form');
|
||||||
const editSpecForm = document.getElementById('edit-specialty-form');
|
const editSpecForm = document.getElementById('edit-specialty-form');
|
||||||
const editProfileForm = document.getElementById('edit-profile-form');
|
|
||||||
const editDeptModal = document.getElementById('modal-edit-department');
|
const editDeptModal = document.getElementById('modal-edit-department');
|
||||||
const editSpecModal = document.getElementById('modal-edit-specialty');
|
const editSpecModal = document.getElementById('modal-edit-specialty');
|
||||||
const editProfileModal = document.getElementById('modal-edit-profile');
|
|
||||||
const editDeptClose = document.getElementById('modal-edit-department-close');
|
const editDeptClose = document.getElementById('modal-edit-department-close');
|
||||||
const editSpecClose = document.getElementById('modal-edit-specialty-close');
|
const editSpecClose = document.getElementById('modal-edit-specialty-close');
|
||||||
const editProfileClose = document.getElementById('modal-edit-profile-close');
|
|
||||||
|
|
||||||
let departments = [];
|
let departments = [];
|
||||||
let specialties = [];
|
let specialties = [];
|
||||||
let profiles = [];
|
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
// Load Departments
|
// Load Departments
|
||||||
@@ -37,11 +30,8 @@ export async function initDepartmentsData() {
|
|||||||
try {
|
try {
|
||||||
specialties = await api.get('/api/specialties');
|
specialties = await api.get('/api/specialties');
|
||||||
renderSpecialties();
|
renderSpecialties();
|
||||||
renderProfileSpecialtySelect();
|
|
||||||
await loadProfiles();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
specTbody.innerHTML = '<tr><td colspan="4" class="loading-row">-</td></tr>';
|
specTbody.innerHTML = '<tr><td colspan="4" class="loading-row">-</td></tr>';
|
||||||
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">-</td></tr>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,52 +71,6 @@ export async function initDepartmentsData() {
|
|||||||
`).join('');
|
`).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) => {
|
createDeptForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hideAlert('create-dept-alert');
|
hideAlert('create-dept-alert');
|
||||||
@@ -197,59 +141,6 @@ 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) => {
|
specTbody.addEventListener('click', async (e) => {
|
||||||
const editBtn = e.target.closest('.btn-edit-specialty');
|
const editBtn = e.target.closest('.btn-edit-specialty');
|
||||||
const deleteBtn = e.target.closest('.btn-delete-specialty');
|
const deleteBtn = e.target.closest('.btn-delete-specialty');
|
||||||
@@ -322,45 +213,14 @@ 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'));
|
editDeptClose.addEventListener('click', () => editDeptModal.classList.remove('open'));
|
||||||
editSpecClose.addEventListener('click', () => editSpecModal.classList.remove('open'));
|
editSpecClose.addEventListener('click', () => editSpecModal.classList.remove('open'));
|
||||||
editProfileClose.addEventListener('click', () => editProfileModal.classList.remove('open'));
|
|
||||||
editDeptModal.addEventListener('click', (e) => {
|
editDeptModal.addEventListener('click', (e) => {
|
||||||
if (e.target === editDeptModal) editDeptModal.classList.remove('open');
|
if (e.target === editDeptModal) editDeptModal.classList.remove('open');
|
||||||
});
|
});
|
||||||
editSpecModal.addEventListener('click', (e) => {
|
editSpecModal.addEventListener('click', (e) => {
|
||||||
if (e.target === editSpecModal) editSpecModal.classList.remove('open');
|
if (e.target === editSpecModal) editSpecModal.classList.remove('open');
|
||||||
});
|
});
|
||||||
editProfileModal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === editProfileModal) editProfileModal.classList.remove('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
await loadData();
|
await loadData();
|
||||||
|
|
||||||
function syncSelect(select) {
|
|
||||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export async function initGroups() {
|
|||||||
String(calendar.academicYearId) === String(yearId)
|
String(calendar.academicYearId) === String(yearId)
|
||||||
&& String(calendar.specialtyId) === String(group.specialtyId || group.specialityCode)
|
&& String(calendar.specialtyId) === String(group.specialtyId || group.specialityCode)
|
||||||
&& String(calendar.specialtyProfileId) === String(group.specialtyProfileId)
|
&& String(calendar.specialtyProfileId) === String(group.specialtyProfileId)
|
||||||
|
&& String(calendar.studyFormId) === String(group.educationFormId)
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
calendarSelect.innerHTML = matching.length
|
calendarSelect.innerHTML = matching.length
|
||||||
|
|||||||
192
frontend/admin/js/views/profiles.js
Normal file
192
frontend/admin/js/views/profiles.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { api } from '../api.js';
|
||||||
|
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||||
|
|
||||||
|
export async function initProfiles() {
|
||||||
|
const createProfileForm = document.getElementById('create-profile-form');
|
||||||
|
const profileSpecialtySelect = document.getElementById('profile-specialty');
|
||||||
|
const profileNameInput = document.getElementById('profile-name');
|
||||||
|
const profileDescriptionInput = document.getElementById('profile-description');
|
||||||
|
const profilesTbody = document.getElementById('profiles-tbody');
|
||||||
|
const profilesContext = document.getElementById('profiles-context');
|
||||||
|
const refreshButton = document.getElementById('profiles-refresh');
|
||||||
|
const editProfileForm = document.getElementById('edit-profile-form');
|
||||||
|
const editProfileModal = document.getElementById('modal-edit-profile');
|
||||||
|
const editProfileClose = document.getElementById('modal-edit-profile-close');
|
||||||
|
|
||||||
|
let specialties = [];
|
||||||
|
let profiles = [];
|
||||||
|
|
||||||
|
await loadSpecialties();
|
||||||
|
|
||||||
|
profileSpecialtySelect.addEventListener('change', loadProfiles);
|
||||||
|
refreshButton.addEventListener('click', loadProfiles);
|
||||||
|
|
||||||
|
createProfileForm.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
hideAlert('create-profile-alert');
|
||||||
|
|
||||||
|
const specialtyId = profileSpecialtySelect.value;
|
||||||
|
const name = profileNameInput.value.trim();
|
||||||
|
const description = profileDescriptionInput.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 (event) => {
|
||||||
|
const editButton = event.target.closest('.btn-edit-profile');
|
||||||
|
const deleteButton = event.target.closest('.btn-delete-profile');
|
||||||
|
|
||||||
|
if (editButton) {
|
||||||
|
const profile = profiles.find(item => String(item.id) === String(editButton.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 (deleteButton) {
|
||||||
|
if (!confirm('Удалить профиль обучения?')) return;
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/specialties/${profileSpecialtySelect.value}/profiles/${deleteButton.dataset.id}`);
|
||||||
|
showAlert('create-profile-alert', 'Профиль удалён', 'success');
|
||||||
|
await loadProfiles();
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('create-profile-alert', error.message || 'Ошибка удаления профиля', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editProfileForm.addEventListener('submit', async (event) => {
|
||||||
|
event.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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editProfileClose.addEventListener('click', () => editProfileModal.classList.remove('open'));
|
||||||
|
editProfileModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === editProfileModal) {
|
||||||
|
editProfileModal.classList.remove('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadSpecialties() {
|
||||||
|
profilesTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
|
||||||
|
try {
|
||||||
|
specialties = await api.get('/api/specialties');
|
||||||
|
renderSpecialtySelect();
|
||||||
|
await loadProfiles();
|
||||||
|
} catch (error) {
|
||||||
|
profileSpecialtySelect.innerHTML = '<option value="">Не удалось загрузить специальности</option>';
|
||||||
|
profilesTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||||
|
syncSelect(profileSpecialtySelect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSpecialtySelect() {
|
||||||
|
if (!specialties.length) {
|
||||||
|
profileSpecialtySelect.innerHTML = '<option value="">Сначала создайте специальность</option>';
|
||||||
|
syncSelect(profileSpecialtySelect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = profileSpecialtySelect.value;
|
||||||
|
profileSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
||||||
|
specialties.map(specialty => {
|
||||||
|
const code = specialty.specialityCode || specialty.specialtyCode || specialty.specialty_code || specialty.id;
|
||||||
|
const name = specialty.specialityName || specialty.name || 'Без названия';
|
||||||
|
return `<option value="${specialty.id}">${escapeHtml(code)} — ${escapeHtml(name)}</option>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (current && specialties.some(specialty => String(specialty.id) === String(current))) {
|
||||||
|
profileSpecialtySelect.value = current;
|
||||||
|
} else {
|
||||||
|
profileSpecialtySelect.value = String(specialties[0].id);
|
||||||
|
}
|
||||||
|
syncSelect(profileSpecialtySelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProfiles() {
|
||||||
|
const specialtyId = profileSpecialtySelect.value;
|
||||||
|
const specialty = specialties.find(item => String(item.id) === String(specialtyId));
|
||||||
|
profilesContext.textContent = specialty ? specialtyLabel(specialty) : '';
|
||||||
|
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function specialtyLabel(specialty) {
|
||||||
|
const code = specialty.specialityCode || specialty.specialtyCode || specialty.specialty_code || specialty.id;
|
||||||
|
const name = specialty.specialityName || specialty.name || 'Без названия';
|
||||||
|
return `${code} · ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSelect(select) {
|
||||||
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,49 +48,6 @@ export async function initSchedule() {
|
|||||||
const timeSlotsTbody = document.getElementById('time-slots-tbody');
|
const timeSlotsTbody = document.getElementById('time-slots-tbody');
|
||||||
const refreshSlotsButton = document.getElementById('time-slots-refresh');
|
const refreshSlotsButton = document.getElementById('time-slots-refresh');
|
||||||
|
|
||||||
const academicYearForm = document.getElementById('academic-year-form');
|
|
||||||
const academicYearFormTitle = document.getElementById('academic-year-form-title');
|
|
||||||
const academicYearIdInput = document.getElementById('academic-year-id');
|
|
||||||
const academicYearTitleInput = document.getElementById('academic-year-title');
|
|
||||||
const academicYearStartInput = document.getElementById('academic-year-start');
|
|
||||||
const academicYearEndInput = document.getElementById('academic-year-end');
|
|
||||||
const academicYearResetButton = document.getElementById('academic-year-reset');
|
|
||||||
const academicYearsTbody = document.getElementById('academic-years-tbody');
|
|
||||||
const academicYearsRefreshButton = document.getElementById('academic-years-refresh');
|
|
||||||
|
|
||||||
const semesterForm = document.getElementById('semester-form');
|
|
||||||
const semesterFormTitle = document.getElementById('semester-form-title');
|
|
||||||
const semesterIdInput = document.getElementById('semester-id');
|
|
||||||
const semesterYearSelect = document.getElementById('semester-year');
|
|
||||||
const semesterTypeSelect = document.getElementById('semester-type');
|
|
||||||
const semesterStartInput = document.getElementById('semester-start');
|
|
||||||
const semesterEndInput = document.getElementById('semester-end');
|
|
||||||
const semesterResetButton = document.getElementById('semester-reset');
|
|
||||||
|
|
||||||
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 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 rules = [];
|
||||||
let subjects = [];
|
let subjects = [];
|
||||||
let groups = [];
|
let groups = [];
|
||||||
@@ -98,22 +55,15 @@ export async function initSchedule() {
|
|||||||
let classrooms = [];
|
let classrooms = [];
|
||||||
let lessonTypes = [];
|
let lessonTypes = [];
|
||||||
let timeSlots = [];
|
let timeSlots = [];
|
||||||
let academicYears = [];
|
|
||||||
let semesters = [];
|
let semesters = [];
|
||||||
let specialties = [];
|
|
||||||
let profilesBySpecialty = new Map();
|
|
||||||
let studyForms = [];
|
|
||||||
let activityTypes = [];
|
|
||||||
let calendars = [];
|
|
||||||
let currentGridRows = [];
|
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears(), loadStudyForms(), loadActivityTypes()]);
|
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears()]);
|
||||||
renderRuleGroups([]);
|
renderRuleGroups([]);
|
||||||
renderRuleSlots([{}]);
|
renderRuleSlots([{}]);
|
||||||
await Promise.all([loadRules(), loadCalendars()]);
|
await loadRules();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlert('schedule-rule-alert', error.message || 'Ошибка загрузки данных расписания', 'error');
|
showAlert('schedule-rule-alert', error.message || 'Ошибка загрузки данных расписания', 'error');
|
||||||
}
|
}
|
||||||
@@ -121,18 +71,9 @@ export async function initSchedule() {
|
|||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
refreshRulesButton?.addEventListener('click', loadRules);
|
refreshRulesButton?.addEventListener('click', loadRules);
|
||||||
refreshSlotsButton?.addEventListener('click', loadTimeSlots);
|
refreshSlotsButton?.addEventListener('click', loadTimeSlots);
|
||||||
academicYearsRefreshButton?.addEventListener('click', loadYears);
|
|
||||||
calendarsRefreshButton?.addEventListener('click', loadCalendars);
|
|
||||||
ruleResetButton?.addEventListener('click', resetRuleForm);
|
ruleResetButton?.addEventListener('click', resetRuleForm);
|
||||||
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
|
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
|
||||||
timeSlotResetButton?.addEventListener('click', resetTimeSlotForm);
|
timeSlotResetButton?.addEventListener('click', resetTimeSlotForm);
|
||||||
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
|
|
||||||
semesterResetButton?.addEventListener('click', resetSemesterForm);
|
|
||||||
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) => {
|
ruleSlotsContainer.addEventListener('click', (event) => {
|
||||||
const removeButton = event.target.closest('.schedule-slot-remove');
|
const removeButton = event.target.closest('.schedule-slot-remove');
|
||||||
@@ -142,44 +83,27 @@ export async function initSchedule() {
|
|||||||
renderRuleSlots(slots.length ? slots : [{}]);
|
renderRuleSlots(slots.length ? slots : [{}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
calendarGrid.addEventListener('change', (event) => {
|
|
||||||
if (event.target.matches('.calendar-day-activity')) {
|
|
||||||
renderCalendarTotals();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ruleForm.addEventListener('submit', saveRule);
|
ruleForm.addEventListener('submit', saveRule);
|
||||||
timeSlotForm.addEventListener('submit', saveTimeSlot);
|
timeSlotForm.addEventListener('submit', saveTimeSlot);
|
||||||
academicYearForm.addEventListener('submit', saveAcademicYear);
|
|
||||||
semesterForm.addEventListener('submit', saveSemester);
|
|
||||||
calendarForm.addEventListener('submit', saveCalendar);
|
|
||||||
|
|
||||||
rulesTbody.addEventListener('click', handleRuleTableClick);
|
rulesTbody.addEventListener('click', handleRuleTableClick);
|
||||||
timeSlotsTbody.addEventListener('click', handleTimeSlotTableClick);
|
timeSlotsTbody.addEventListener('click', handleTimeSlotTableClick);
|
||||||
academicYearsTbody.addEventListener('click', handleAcademicYearTableClick);
|
|
||||||
calendarsTbody.addEventListener('click', handleCalendarTableClick);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBaseLists() {
|
async function loadBaseLists() {
|
||||||
[subjects, groups, teachers, classrooms, lessonTypes, specialties] = await Promise.all([
|
[subjects, groups, teachers, classrooms, lessonTypes] = await Promise.all([
|
||||||
api.get('/api/subjects'),
|
api.get('/api/subjects'),
|
||||||
api.get('/api/groups'),
|
api.get('/api/groups'),
|
||||||
api.get('/api/users/teachers'),
|
api.get('/api/users/teachers'),
|
||||||
api.get('/api/classrooms'),
|
api.get('/api/classrooms'),
|
||||||
api.get('/api/lesson-types'),
|
api.get('/api/lesson-types')
|
||||||
api.get('/api/specialties')
|
|
||||||
]);
|
]);
|
||||||
await loadProfiles();
|
|
||||||
populateSubjectSelects();
|
populateSubjectSelects();
|
||||||
populateSpecialtySelects();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProfiles() {
|
async function loadYears() {
|
||||||
const pairs = await Promise.all(specialties.map(async (speciality) => {
|
const academicYears = await api.get('/api/admin/calendar/years');
|
||||||
const profiles = await api.get(`/api/specialties/${speciality.id}/profiles`);
|
semesters = academicYears.flatMap(year => year.semesters || []);
|
||||||
return [String(speciality.id), profiles];
|
populateSemesterSelects();
|
||||||
}));
|
|
||||||
profilesBySpecialty = new Map(pairs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRules() {
|
async function loadRules() {
|
||||||
@@ -418,6 +342,7 @@ export async function initSchedule() {
|
|||||||
showAlert('time-slot-alert', 'Временной слот сохранён', 'success');
|
showAlert('time-slot-alert', 'Временной слот сохранён', 'success');
|
||||||
resetTimeSlotForm();
|
resetTimeSlotForm();
|
||||||
await loadTimeSlots();
|
await loadTimeSlots();
|
||||||
|
renderRuleSlots(readRuleSlots(false).length ? readRuleSlots(false) : [{}]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error');
|
showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error');
|
||||||
}
|
}
|
||||||
@@ -426,6 +351,7 @@ export async function initSchedule() {
|
|||||||
async function handleTimeSlotTableClick(event) {
|
async function handleTimeSlotTableClick(event) {
|
||||||
const editButton = event.target.closest('.btn-edit-time-slot');
|
const editButton = event.target.closest('.btn-edit-time-slot');
|
||||||
const deleteButton = event.target.closest('.btn-delete-time-slot');
|
const deleteButton = event.target.closest('.btn-delete-time-slot');
|
||||||
|
|
||||||
if (editButton) {
|
if (editButton) {
|
||||||
const slot = timeSlots.find(item => item.id == editButton.dataset.id);
|
const slot = timeSlots.find(item => item.id == editButton.dataset.id);
|
||||||
if (!slot) return;
|
if (!slot) return;
|
||||||
@@ -439,12 +365,14 @@ export async function initSchedule() {
|
|||||||
timeSlotForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
timeSlotForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteButton) {
|
if (deleteButton) {
|
||||||
if (!confirm('Удалить временной слот?')) return;
|
if (!confirm('Удалить временной слот?')) return;
|
||||||
try {
|
try {
|
||||||
await api.delete('/api/admin/time-slots/' + deleteButton.dataset.id);
|
await api.delete('/api/admin/time-slots/' + deleteButton.dataset.id);
|
||||||
showAlert('time-slot-alert', 'Временной слот удалён', 'success');
|
showAlert('time-slot-alert', 'Временной слот удалён', 'success');
|
||||||
await loadTimeSlots();
|
await loadTimeSlots();
|
||||||
|
renderRuleSlots(readRuleSlots(false).length ? readRuleSlots(false) : [{}]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlert('time-slot-alert', error.message || 'Ошибка удаления временного слота', 'error');
|
showAlert('time-slot-alert', error.message || 'Ошибка удаления временного слота', 'error');
|
||||||
}
|
}
|
||||||
@@ -458,426 +386,10 @@ export async function initSchedule() {
|
|||||||
hideAlert('time-slot-alert');
|
hideAlert('time-slot-alert');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadYears() {
|
|
||||||
academicYearsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
|
|
||||||
try {
|
|
||||||
academicYears = await api.get('/api/admin/calendar/years');
|
|
||||||
semesters = academicYears.flatMap(year => year.semesters || []);
|
|
||||||
renderAcademicYears();
|
|
||||||
populateYearSelects();
|
|
||||||
populateSemesterSelects();
|
|
||||||
} catch (error) {
|
|
||||||
academicYearsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAcademicYears() {
|
|
||||||
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>${renderSemesterList(year.semesters || [])}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button>
|
|
||||||
<button class="btn-delete btn-delete-year" data-id="${year.id}">Удалить</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSemesterList(items) {
|
|
||||||
if (!items.length) return '<span class="muted">Нет семестров</span>';
|
|
||||||
return items.map(semester => `
|
|
||||||
<div class="schedule-semester-line">
|
|
||||||
${escapeHtml(semesterLabel(semester))}
|
|
||||||
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}">Изменить</button>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
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('Заполните название и даты учебного года');
|
|
||||||
const id = academicYearIdInput.value;
|
|
||||||
await (id
|
|
||||||
? api.put('/api/admin/calendar/years/' + id, payload)
|
|
||||||
: api.post('/api/admin/calendar/years', payload));
|
|
||||||
showAlert('academic-year-alert', 'Учебный год сохранён', 'success');
|
|
||||||
resetAcademicYearForm();
|
|
||||||
await loadYears();
|
|
||||||
await loadCalendars();
|
|
||||||
} catch (error) {
|
|
||||||
showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAcademicYearTableClick(event) {
|
|
||||||
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;
|
|
||||||
academicYearFormTitle.textContent = 'Редактирование учебного года';
|
|
||||||
academicYearIdInput.value = year.id;
|
|
||||||
academicYearTitleInput.value = year.title || '';
|
|
||||||
academicYearStartInput.value = year.startDate || '';
|
|
||||||
academicYearEndInput.value = year.endDate || '';
|
|
||||||
hideAlert('academic-year-alert');
|
|
||||||
academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (editSemesterButton) {
|
|
||||||
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
|
|
||||||
if (!semester) return;
|
|
||||||
semesterFormTitle.textContent = 'Редактирование семестра';
|
|
||||||
semesterIdInput.value = semester.id;
|
|
||||||
semesterYearSelect.value = semester.academicYearId || '';
|
|
||||||
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
|
|
||||||
semesterStartInput.value = semester.startDate || '';
|
|
||||||
semesterEndInput.value = semester.endDate || '';
|
|
||||||
syncSelects(semesterYearSelect, semesterTypeSelect);
|
|
||||||
hideAlert('semester-alert');
|
|
||||||
semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deleteYearButton) {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetAcademicYearForm() {
|
|
||||||
academicYearForm.reset();
|
|
||||||
academicYearFormTitle.textContent = 'Учебный год';
|
|
||||||
academicYearIdInput.value = '';
|
|
||||||
hideAlert('academic-year-alert');
|
|
||||||
}
|
|
||||||
|
|
||||||
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('Заполните учебный год, тип и даты семестра');
|
|
||||||
const id = semesterIdInput.value;
|
|
||||||
await (id
|
|
||||||
? api.put('/api/admin/calendar/semesters/' + id, payload)
|
|
||||||
: api.post(`/api/admin/calendar/years/${yearId}/semesters`, payload));
|
|
||||||
showAlert('semester-alert', 'Семестр сохранён', 'success');
|
|
||||||
resetSemesterForm();
|
|
||||||
await loadYears();
|
|
||||||
} catch (error) {
|
|
||||||
showAlert('semester-alert', error.message || 'Ошибка сохранения семестра', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetSemesterForm() {
|
|
||||||
semesterForm.reset();
|
|
||||||
semesterFormTitle.textContent = 'Семестр';
|
|
||||||
semesterIdInput.value = '';
|
|
||||||
syncSelects(semesterYearSelect, semesterTypeSelect);
|
|
||||||
hideAlert('semester-alert');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStudyForms() {
|
|
||||||
studyForms = await api.get('/api/admin/academic-calendars/study-forms');
|
|
||||||
populateStudyFormSelect();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
calendars = await api.get('/api/admin/academic-calendars');
|
|
||||||
renderCalendars();
|
|
||||||
populateEditorCalendarSelect();
|
|
||||||
} catch (error) {
|
|
||||||
calendarsTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCalendars() {
|
|
||||||
if (!calendars.length) {
|
|
||||||
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Календарные графики не созданы</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
calendarsTbody.innerHTML = calendars.map(calendar => `
|
|
||||||
<tr>
|
|
||||||
<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-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 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 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;
|
|
||||||
try {
|
|
||||||
await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
|
|
||||||
showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
|
|
||||||
await loadCalendars();
|
|
||||||
} catch (error) {
|
|
||||||
showAlert('academic-calendar-alert', error.message || 'Ошибка удаления календарного графика', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 loadCalendarGrid() {
|
|
||||||
hideAlert('calendar-editor-alert');
|
|
||||||
const calendar = selectedEditorCalendar();
|
|
||||||
if (!calendar) {
|
|
||||||
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
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('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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('calendar-editor-alert', 'Сначала загрузите сетку графика', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.put(`/api/admin/academic-calendars/${calendar.id}/grid`, rows);
|
|
||||||
showAlert('calendar-editor-alert', 'Календарный учебный график сохранён', 'success');
|
|
||||||
await loadCalendarGrid();
|
|
||||||
} catch (error) {
|
|
||||||
showAlert('calendar-editor-alert', error.message || 'Ошибка сохранения сетки', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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() {
|
function populateSubjectSelects() {
|
||||||
ruleSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
ruleSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||||
subjects.map(subject => `<option value="${subject.id}">${escapeHtml(subject.name)}</option>`).join('');
|
subjects.map(subject => `<option value="${subject.id}">${escapeHtml(subject.name)}</option>`).join('');
|
||||||
}
|
syncSelects(ruleSubjectSelect);
|
||||||
|
|
||||||
function populateYearSelects() {
|
|
||||||
const yearOptions = '<option value="">Выберите учебный год</option>' +
|
|
||||||
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
|
||||||
semesterYearSelect.innerHTML = yearOptions;
|
|
||||||
calendarYearSelect.innerHTML = yearOptions;
|
|
||||||
syncSelects(semesterYearSelect, calendarYearSelect);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateSemesterSelects() {
|
function populateSemesterSelects() {
|
||||||
@@ -887,57 +399,6 @@ export async function initSchedule() {
|
|||||||
syncSelects(ruleSemesterSelect);
|
syncSelects(ruleSemesterSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateSpecialtySelects() {
|
|
||||||
calendarSpecialtySelect.innerHTML = '<option value="">Выберите специальность</option>' +
|
|
||||||
specialties.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality))}</option>`).join('');
|
|
||||||
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) {
|
function options(items, selectedValue) {
|
||||||
const selected = selectedValue == null ? '' : String(selectedValue);
|
const selected = selectedValue == null ? '' : String(selectedValue);
|
||||||
return '<option value="">Выберите</option>' + items.map(item => {
|
return '<option value="">Выберите</option>' + items.map(item => {
|
||||||
@@ -946,10 +407,6 @@ export async function initSchedule() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function toActivityOption(type) {
|
|
||||||
return { value: type.id, label: `${type.code} - ${type.name}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
function toTimeSlotOption(slot) {
|
function toTimeSlotOption(slot) {
|
||||||
return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` };
|
return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` };
|
||||||
}
|
}
|
||||||
@@ -967,22 +424,6 @@ export async function initSchedule() {
|
|||||||
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
|
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) {
|
function semesterLabel(semester) {
|
||||||
const type = normalizeSemesterType(semester.semesterType);
|
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})`;
|
||||||
@@ -997,45 +438,11 @@ export async function initSchedule() {
|
|||||||
return String(type || '').toLowerCase();
|
return String(type || '').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function trimTime(value) {
|
function trimTime(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
return String(value).slice(0, 5);
|
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) {
|
function syncSelects(...selects) {
|
||||||
selects.filter(Boolean).forEach(select => {
|
selects.filter(Boolean).forEach(select => {
|
||||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|||||||
225
frontend/admin/views/academic-calendar.html
Normal file
225
frontend/admin/views/academic-calendar.html
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<div class="card create-card">
|
||||||
|
<h2 id="academic-year-form-title">Учебный год</h2>
|
||||||
|
<form id="academic-year-form" novalidate>
|
||||||
|
<input type="hidden" id="academic-year-id">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="academic-year-title">Название</label>
|
||||||
|
<input type="text" id="academic-year-title" placeholder="2026-2027" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="academic-year-start">Начало</label>
|
||||||
|
<input type="date" id="academic-year-start" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="academic-year-end">Окончание</label>
|
||||||
|
<input type="date" id="academic-year-end" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Сохранить год</button>
|
||||||
|
<button type="button" class="btn-edit-classroom" id="academic-year-reset">Очистить</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-alert" id="academic-year-alert" role="alert"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header-row">
|
||||||
|
<h2>Учебные годы</h2>
|
||||||
|
<button class="btn-primary" id="academic-years-refresh">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Период</th>
|
||||||
|
<th>Семестры</th>
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="academic-years-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card create-card">
|
||||||
|
<h2 id="semester-form-title">Семестр</h2>
|
||||||
|
<form id="semester-form" novalidate>
|
||||||
|
<input type="hidden" id="semester-id">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="semester-year">Учебный год</label>
|
||||||
|
<select id="semester-year" required>
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="semester-type">Тип семестра</label>
|
||||||
|
<select id="semester-type" required>
|
||||||
|
<option value="autumn">Осенний</option>
|
||||||
|
<option value="spring">Весенний</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="semester-start">Начало</label>
|
||||||
|
<input type="date" id="semester-start" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="semester-end">Окончание</label>
|
||||||
|
<input type="date" id="semester-end" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Сохранить семестр</button>
|
||||||
|
<button type="button" class="btn-edit-classroom" id="semester-reset">Очистить</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-alert" id="semester-alert" role="alert"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card create-card">
|
||||||
|
<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="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="academic-calendar-specialty">Специальность</label>
|
||||||
|
<select id="academic-calendar-specialty" required>
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="academic-calendar-profile">Профиль</label>
|
||||||
|
<select id="academic-calendar-profile" required>
|
||||||
|
<option value="">Выберите специальность</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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="academic-calendar-alert" role="alert"></div>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="academic-calendars-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="loading-row">Загрузка...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card create-card">
|
||||||
|
<h2>Редактор годового графика</h2>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="calendar-editor-calendar">График</label>
|
||||||
|
<select id="calendar-editor-calendar">
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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="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>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="calendar-day-dialog">
|
||||||
|
<div class="modal-content card calendar-day-dialog-card">
|
||||||
|
<h2>Ячейка графика</h2>
|
||||||
|
<button class="modal-close" id="calendar-day-dialog-close">×</button>
|
||||||
|
<div class="calendar-day-dialog-grid">
|
||||||
|
<div>
|
||||||
|
<span>Дата</span>
|
||||||
|
<strong id="calendar-day-dialog-date">-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Курс</span>
|
||||||
|
<strong id="calendar-day-dialog-course">-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Неделя</span>
|
||||||
|
<strong id="calendar-day-dialog-week">-</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>День</span>
|
||||||
|
<strong id="calendar-day-dialog-day">-</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-day-dialog-activity" id="calendar-day-dialog-activity-summary"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="calendar-day-dialog-activity">Код активности</label>
|
||||||
|
<select id="calendar-day-dialog-activity">
|
||||||
|
<option value="">Выберите код</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -40,47 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="card create-card">
|
||||||
<h2>Создание специальности</h2>
|
<h2>Создание специальности</h2>
|
||||||
<form id="create-specialty-form">
|
<form id="create-specialty-form">
|
||||||
@@ -165,25 +124,3 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
|
|||||||
72
frontend/admin/views/profiles.html
Normal file
72
frontend/admin/views/profiles.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<div class="card create-card">
|
||||||
|
<div class="card-header-row">
|
||||||
|
<h2>Создание профиля обучения</h2>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header-row">
|
||||||
|
<h2>Профили выбранной специальности</h2>
|
||||||
|
<button class="btn-primary" id="profiles-refresh" type="button">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div class="profiles-context" id="profiles-context"></div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<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="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>
|
||||||
@@ -130,197 +130,3 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card create-card">
|
|
||||||
<h2 id="academic-year-form-title">Учебный год</h2>
|
|
||||||
<form id="academic-year-form" novalidate>
|
|
||||||
<input type="hidden" id="academic-year-id">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="academic-year-title">Название</label>
|
|
||||||
<input type="text" id="academic-year-title" placeholder="2026-2027" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="academic-year-start">Начало</label>
|
|
||||||
<input type="date" id="academic-year-start" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="academic-year-end">Окончание</label>
|
|
||||||
<input type="date" id="academic-year-end" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary">Сохранить год</button>
|
|
||||||
<button type="button" class="btn-edit-classroom" id="academic-year-reset">Очистить</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-alert" id="academic-year-alert" role="alert"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header-row">
|
|
||||||
<h2>Учебные годы</h2>
|
|
||||||
<button class="btn-primary" id="academic-years-refresh">Обновить</button>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Название</th>
|
|
||||||
<th>Период</th>
|
|
||||||
<th>Семестры</th>
|
|
||||||
<th>Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="academic-years-tbody">
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card create-card">
|
|
||||||
<h2 id="semester-form-title">Семестр</h2>
|
|
||||||
<form id="semester-form" novalidate>
|
|
||||||
<input type="hidden" id="semester-id">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="semester-year">Учебный год</label>
|
|
||||||
<select id="semester-year" required>
|
|
||||||
<option value="">Загрузка...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="semester-type">Тип семестра</label>
|
|
||||||
<select id="semester-type" required>
|
|
||||||
<option value="autumn">Осенний</option>
|
|
||||||
<option value="spring">Весенний</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="semester-start">Начало</label>
|
|
||||||
<input type="date" id="semester-start" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="semester-end">Окончание</label>
|
|
||||||
<input type="date" id="semester-end" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary">Сохранить семестр</button>
|
|
||||||
<button type="button" class="btn-edit-classroom" id="semester-reset">Очистить</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-alert" id="semester-alert" role="alert"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card create-card">
|
|
||||||
<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="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="academic-calendar-specialty">Специальность</label>
|
|
||||||
<select id="academic-calendar-specialty" required>
|
|
||||||
<option value="">Загрузка...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="academic-calendar-profile">Профиль</label>
|
|
||||||
<select id="academic-calendar-profile" required>
|
|
||||||
<option value="">Выберите специальность</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<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="academic-calendar-alert" role="alert"></div>
|
|
||||||
</form>
|
|
||||||
</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>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="academic-calendars-tbody">
|
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="loading-row">Загрузка...</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card create-card">
|
|
||||||
<h2>Редактор годового графика</h2>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="calendar-editor-calendar">График</label>
|
|
||||||
<select id="calendar-editor-calendar">
|
|
||||||
<option value="">Загрузка...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<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="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