Изменен фильтр в "группы обучения", изменена механика создания подгрупп
This commit is contained in:
@@ -66,6 +66,10 @@ public class SubgroupController {
|
|||||||
if (subgroupRepository.existsByStudentGroupIdAndNameIgnoreCase(groupId, request.name().trim())) {
|
if (subgroupRepository.existsByStudentGroupIdAndNameIgnoreCase(groupId, request.name().trim())) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
||||||
}
|
}
|
||||||
|
String capacityError = validateGroupCapacity(group, null, request.studentCapacity());
|
||||||
|
if (capacityError != null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", capacityError));
|
||||||
|
}
|
||||||
|
|
||||||
Subgroup subgroup = new Subgroup();
|
Subgroup subgroup = new Subgroup();
|
||||||
subgroup.setStudentGroup(group);
|
subgroup.setStudentGroup(group);
|
||||||
@@ -95,6 +99,10 @@ public class SubgroupController {
|
|||||||
if (duplicateName) {
|
if (duplicateName) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
||||||
}
|
}
|
||||||
|
String capacityError = validateGroupCapacity(subgroup.getStudentGroup(), id, request.studentCapacity());
|
||||||
|
if (capacityError != null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", capacityError));
|
||||||
|
}
|
||||||
|
|
||||||
subgroup.setName(newName);
|
subgroup.setName(newName);
|
||||||
subgroup.setStudentCapacity(request.studentCapacity());
|
subgroup.setStudentCapacity(request.studentCapacity());
|
||||||
@@ -113,6 +121,10 @@ public class SubgroupController {
|
|||||||
if (scheduleRuleSlotRepository.existsBySubgroupId(id)) {
|
if (scheduleRuleSlotRepository.existsBySubgroupId(id)) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
||||||
}
|
}
|
||||||
|
String deleteValidationError = validateDeleteKeepsCompleteSplit(subgroup);
|
||||||
|
if (deleteValidationError != null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", deleteValidationError));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
subgroup.archive("Подгруппа архивирована");
|
subgroup.archive("Подгруппа архивирована");
|
||||||
subgroupRepository.save(subgroup);
|
subgroupRepository.save(subgroup);
|
||||||
@@ -127,8 +139,39 @@ public class SubgroupController {
|
|||||||
if (request == null || request.name() == null || request.name().isBlank()) {
|
if (request == null || request.name() == null || request.name().isBlank()) {
|
||||||
return "Название подгруппы обязательно";
|
return "Название подгруппы обязательно";
|
||||||
}
|
}
|
||||||
if (request.studentCapacity() != null && request.studentCapacity() < 0) {
|
if (request.studentCapacity() == null) {
|
||||||
return "Численность подгруппы не может быть отрицательной";
|
return "Численность подгруппы обязательна";
|
||||||
|
}
|
||||||
|
if (request.studentCapacity() <= 0) {
|
||||||
|
return "Численность подгруппы должна быть больше 0";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String validateGroupCapacity(StudentGroup group, Long excludedSubgroupId, Integer requestedCapacity) {
|
||||||
|
Long currentCapacity = excludedSubgroupId == null
|
||||||
|
? subgroupRepository.sumActiveStudentCapacityByGroupId(group.getId())
|
||||||
|
: subgroupRepository.sumActiveStudentCapacityByGroupIdExcludingId(group.getId(), excludedSubgroupId);
|
||||||
|
long totalCapacity = currentCapacity + requestedCapacity.longValue();
|
||||||
|
if (totalCapacity > group.getGroupSize()) {
|
||||||
|
return "Сумма численностей подгрупп не может превышать численность группы (" + group.getGroupSize() + " чел.)";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String validateDeleteKeepsCompleteSplit(Subgroup subgroup) {
|
||||||
|
StudentGroup group = subgroup.getStudentGroup();
|
||||||
|
long remainingCount = subgroupRepository.countActiveByGroupIdExcludingId(group.getId(), subgroup.getId());
|
||||||
|
if (remainingCount == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long remainingCapacity = subgroupRepository.sumActiveStudentCapacityByGroupIdExcludingId(
|
||||||
|
group.getId(),
|
||||||
|
subgroup.getId()
|
||||||
|
);
|
||||||
|
if (remainingCapacity < group.getGroupSize()) {
|
||||||
|
return "Нельзя удалить одну подгруппу из деления: измените количество подгрупп в форме настройки или отключите деление";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,32 @@ public interface SubgroupRepository extends JpaRepository<Subgroup, Long> {
|
|||||||
and subgroup.status <> 'ARCHIVED'
|
and subgroup.status <> 'ARCHIVED'
|
||||||
""")
|
""")
|
||||||
boolean existsByStudentGroupIdAndNameIgnoreCase(@Param("groupId") Long groupId, @Param("name") String name);
|
boolean existsByStudentGroupIdAndNameIgnoreCase(@Param("groupId") Long groupId, @Param("name") String name);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select coalesce(sum(subgroup.studentCapacity), 0)
|
||||||
|
from Subgroup subgroup
|
||||||
|
where subgroup.studentGroup.id = :groupId
|
||||||
|
and subgroup.status <> 'ARCHIVED'
|
||||||
|
""")
|
||||||
|
Long sumActiveStudentCapacityByGroupId(@Param("groupId") Long groupId);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select coalesce(sum(subgroup.studentCapacity), 0)
|
||||||
|
from Subgroup subgroup
|
||||||
|
where subgroup.studentGroup.id = :groupId
|
||||||
|
and subgroup.status <> 'ARCHIVED'
|
||||||
|
and subgroup.id <> :excludedId
|
||||||
|
""")
|
||||||
|
Long sumActiveStudentCapacityByGroupIdExcludingId(@Param("groupId") Long groupId,
|
||||||
|
@Param("excludedId") Long excludedId);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select count(subgroup)
|
||||||
|
from Subgroup subgroup
|
||||||
|
where subgroup.studentGroup.id = :groupId
|
||||||
|
and subgroup.status <> 'ARCHIVED'
|
||||||
|
and subgroup.id <> :excludedId
|
||||||
|
""")
|
||||||
|
long countActiveByGroupIdExcludingId(@Param("groupId") Long groupId,
|
||||||
|
@Param("excludedId") Long excludedId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE subgroups
|
||||||
|
DROP CONSTRAINT IF EXISTS subgroups_group_id_name_key;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_subgroups_active_group_name
|
||||||
|
ON subgroups (group_id, lower(name))
|
||||||
|
WHERE status <> 'ARCHIVED';
|
||||||
@@ -758,6 +758,10 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
|
|||||||
|
|
||||||
Подгруппы используются только для деления лабораторных занятий. Лекции и практики не принимают `subgroupId` и `subgroupIds`.
|
Подгруппы используются только для деления лабораторных занятий. Лекции и практики не принимают `subgroupId` и `subgroupIds`.
|
||||||
|
|
||||||
|
Для одной учебной группы сумма численностей активных подгрупп не может превышать численность самой группы. Frontend на вкладке `groups` настраивает деление как один из режимов: без подгрупп, две подгруппы или три подгруппы.
|
||||||
|
Имена подгрупп уникальны только среди активных подгрупп одной группы, поэтому после архивирования можно создать новую `Подгруппа 1`.
|
||||||
|
Частичное удаление подгруппы из активного деления запрещено, если после удаления оставшиеся подгруппы не покрывают всю численность группы. Количество подгрупп меняется через настройку режима деления.
|
||||||
|
|
||||||
| Метод | URL | Назначение |
|
| Метод | URL | Назначение |
|
||||||
|-------|-----|------------|
|
|-------|-----|------------|
|
||||||
| `GET` | `/api/subgroups` | Список всех подгрупп |
|
| `GET` | `/api/subgroups` | Список всех подгрупп |
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
|||||||
|
|
||||||
- **Поля:** Название, численность, форма обучения, кафедра, специальность, профиль обучения, год начала обучения
|
- **Поля:** Название, численность, форма обучения, кафедра, специальность, профиль обучения, год начала обучения
|
||||||
- **Курс:** вычисляется относительно учебного года: `год начала учебного года - year_start_study + 1`, но до начала обучения отдаётся как `0`, а не отрицательное число
|
- **Курс:** вычисляется относительно учебного года: `год начала учебного года - year_start_study + 1`, но до начала обучения отдаётся как `0`, а не отрицательное число
|
||||||
- **Подгруппы:** Возможно деление группы на подгруппы (таблица `subgroups`)
|
- **Подгруппы:** Возможно деление группы на 2 или 3 подгруппы либо режим без деления (таблица `subgroups`). Сумма численностей активных подгрупп не может превышать численность группы. Нельзя удалить одну подгруппу из активного деления так, чтобы часть студентов не относилась ни к одной подгруппе.
|
||||||
- **Календарь:** на каждый учебный год группе назначается конкретный календарный учебный график
|
- **Календарь:** на каждый учебный год группе назначается конкретный календарный учебный график
|
||||||
- **Дисциплины графика:** при назначении графика группе отображаются дисциплины, вручную привязанные к номерам семестров этого графика
|
- **Дисциплины графика:** при назначении графика группе отображаются дисциплины, вручную привязанные к номерам семестров этого графика
|
||||||
- **Завершение обучения:** если текущий курс больше `course_count` назначенного календарного графика, группа считается завершившей обучение и не попадает в обычные списки выбора. Историческое расписание по датам периода обучения остаётся доступным.
|
- **Завершение обучения:** если текущий курс больше `course_count` назначенного календарного графика, группа считается завершившей обучение и не попадает в обычные списки выбора. Историческое расписание по датам периода обучения остаётся доступным.
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ erDiagram
|
|||||||
| `name` | VARCHAR(100) | Название подгруппы |
|
| `name` | VARCHAR(100) | Название подгруппы |
|
||||||
| `student_capacity` | INT | Количество студентов |
|
| `student_capacity` | INT | Количество студентов |
|
||||||
|
|
||||||
Уникальность задаётся парой `(group_id, name)`: в разных группах могут быть подгруппы с одинаковым названием. Подгруппы применяются только для лабораторных занятий.
|
Уникальность активных записей задаётся парой `(group_id, lower(name))`: в разных группах могут быть подгруппы с одинаковым названием, а архивные подгруппы не блокируют повторное создание подгруппы с тем же именем. Подгруппы применяются только для лабораторных занятий.
|
||||||
|
|
||||||
#### `subjects` — Дисциплины
|
#### `subjects` — Дисциплины
|
||||||
| Колонка | Тип | Описание |
|
| Колонка | Тип | Описание |
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ frontend/
|
|||||||
| Tab | Описание | API |
|
| Tab | Описание | API |
|
||||||
|-----|----------|-----|
|
|-----|----------|-----|
|
||||||
| `users` | CRUD пользователей и очередь заявок кафедр на создание преподавателей | `/api/users`, `/api/teacher-requests` |
|
| `users` | CRUD пользователей и очередь заявок кафедр на создание преподавателей | `/api/users`, `/api/teacher-requests` |
|
||||||
| `groups` | CRUD групп, подгруппы лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` |
|
| `groups` | CRUD групп, мультифильтр списка по формам обучения, настройка 0/2/3 подгрупп для лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` |
|
||||||
| `edu-forms` | Формы обучения | `/api/education-forms` |
|
| `edu-forms` | Формы обучения | `/api/education-forms` |
|
||||||
| `profiles` | Создание, редактирование и удаление профилей обучения специальностей | `/api/specialties/{id}/profiles` |
|
| `profiles` | Создание, редактирование и удаление профилей обучения специальностей | `/api/specialties/{id}/profiles` |
|
||||||
| `equipments` | Оборудование | `/api/equipments` |
|
| `equipments` | Оборудование | `/api/equipments` |
|
||||||
|
|||||||
@@ -2255,6 +2255,86 @@ input[type="number"] {
|
|||||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-row .custom-multi-select {
|
||||||
|
width: min(240px, 100%);
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row .select-box {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-row .select-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(190px, 240px) minmax(0, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"count capacities"
|
||||||
|
"actions actions";
|
||||||
|
gap: 1rem 0.75rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-count-field {
|
||||||
|
grid-area: count;
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-fields {
|
||||||
|
grid-area: capacities;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-fields:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-fields-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-fields-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-field {
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-actions {
|
||||||
|
grid-area: actions;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.subgroup-config-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"count"
|
||||||
|
"capacities"
|
||||||
|
"actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
.subgroup-capacity-fields {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===== Custom Multi-Select ===== */
|
/* ===== Custom Multi-Select ===== */
|
||||||
.custom-multi-select {
|
.custom-multi-select {
|
||||||
@@ -2380,7 +2460,8 @@ input[type="number"] {
|
|||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group .checkbox-group-vertical .checkbox-item {
|
.form-group .checkbox-group-vertical .checkbox-item,
|
||||||
|
.filter-row .checkbox-group-vertical .checkbox-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ async function fetchEducationForms() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FILTER_EF_EMPTY_TEXT = 'Выберите форму обучения';
|
||||||
|
|
||||||
export async function initGroups() {
|
export async function initGroups() {
|
||||||
const groupsTbody = document.getElementById('groups-tbody');
|
const groupsTbody = document.getElementById('groups-tbody');
|
||||||
const createGroupForm = document.getElementById('create-group-form');
|
const createGroupForm = document.getElementById('create-group-form');
|
||||||
@@ -17,7 +19,10 @@ export async function initGroups() {
|
|||||||
const newGroupDepartmentSelect = document.getElementById('new-group-department');
|
const newGroupDepartmentSelect = document.getElementById('new-group-department');
|
||||||
const newGroupSpecialitySelect = document.getElementById('new-group-speciality-code');
|
const newGroupSpecialitySelect = document.getElementById('new-group-speciality-code');
|
||||||
const newGroupProfileSelect = document.getElementById('new-group-profile');
|
const newGroupProfileSelect = document.getElementById('new-group-profile');
|
||||||
const filterEfSelect = document.getElementById('filter-ef');
|
const filterEfBox = document.getElementById('filter-ef-box');
|
||||||
|
const filterEfMenu = document.getElementById('filter-ef-menu');
|
||||||
|
const filterEfText = document.getElementById('filter-ef-text');
|
||||||
|
const filterEfCheckboxes = document.getElementById('filter-ef-checkboxes');
|
||||||
|
|
||||||
// Модалка группы
|
// Модалка группы
|
||||||
const modalEditGroup = document.getElementById('modal-edit-group');
|
const modalEditGroup = document.getElementById('modal-edit-group');
|
||||||
@@ -36,8 +41,8 @@ export async function initGroups() {
|
|||||||
const manageSubgroupForm = document.getElementById('manage-subgroup-form');
|
const manageSubgroupForm = document.getElementById('manage-subgroup-form');
|
||||||
const manageSubgroupIdInput = document.getElementById('manage-subgroup-id');
|
const manageSubgroupIdInput = document.getElementById('manage-subgroup-id');
|
||||||
const manageSubgroupGroupIdInput = document.getElementById('manage-subgroup-group-id');
|
const manageSubgroupGroupIdInput = document.getElementById('manage-subgroup-group-id');
|
||||||
const manageSubgroupNameInput = document.getElementById('manage-subgroup-name');
|
const manageSubgroupCountSelect = document.getElementById('manage-subgroup-count');
|
||||||
const manageSubgroupCapacityInput = document.getElementById('manage-subgroup-capacity');
|
const manageSubgroupCapacities = document.getElementById('manage-subgroup-capacities');
|
||||||
const btnSaveSubgroup = document.getElementById('btn-save-subgroup');
|
const btnSaveSubgroup = document.getElementById('btn-save-subgroup');
|
||||||
const btnCancelSubgroupEdit = document.getElementById('btn-cancel-subgroup-edit');
|
const btnCancelSubgroupEdit = document.getElementById('btn-cancel-subgroup-edit');
|
||||||
const manageSubgroupsTbody = document.getElementById('manage-subgroups-tbody');
|
const manageSubgroupsTbody = document.getElementById('manage-subgroups-tbody');
|
||||||
@@ -61,12 +66,14 @@ export async function initGroups() {
|
|||||||
let academicYears = [];
|
let academicYears = [];
|
||||||
let calendars = [];
|
let calendars = [];
|
||||||
let currentAssignments = [];
|
let currentAssignments = [];
|
||||||
|
let activeSubgroupGroup = null;
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
await loadInitialData();
|
await loadInitialData();
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
filterEfSelect.addEventListener('change', applyGroupFilter);
|
initFilterEfDropdown();
|
||||||
|
filterEfCheckboxes.addEventListener('change', handleFilterEfChange);
|
||||||
newGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value));
|
newGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value));
|
||||||
editGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value));
|
editGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value));
|
||||||
|
|
||||||
@@ -75,6 +82,7 @@ export async function initGroups() {
|
|||||||
|
|
||||||
// Модалка подгрупп
|
// Модалка подгрупп
|
||||||
manageSubgroupForm.addEventListener('submit', saveSubgroup);
|
manageSubgroupForm.addEventListener('submit', saveSubgroup);
|
||||||
|
manageSubgroupCountSelect.addEventListener('change', () => renderSubgroupCapacityFields(Number(manageSubgroupCountSelect.value)));
|
||||||
btnCancelSubgroupEdit.addEventListener('click', resetSubgroupForm);
|
btnCancelSubgroupEdit.addEventListener('click', resetSubgroupForm);
|
||||||
manageSubgroupsTbody.addEventListener('click', handleSubgroupTableClick);
|
manageSubgroupsTbody.addEventListener('click', handleSubgroupTableClick);
|
||||||
|
|
||||||
@@ -101,6 +109,37 @@ export async function initGroups() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initFilterEfDropdown() {
|
||||||
|
if (!filterEfBox || !filterEfMenu) return;
|
||||||
|
|
||||||
|
const toggleDropdown = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const isOpen = filterEfMenu.classList.contains('open');
|
||||||
|
document.querySelectorAll('.dropdown-menu').forEach(menu => menu.classList.remove('open'));
|
||||||
|
document.querySelectorAll('.select-box').forEach(box => box.classList.remove('active'));
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
filterEfMenu.classList.add('open');
|
||||||
|
filterEfBox.classList.add('active');
|
||||||
|
filterEfBox.setAttribute('aria-expanded', 'true');
|
||||||
|
} else {
|
||||||
|
filterEfBox.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
filterEfBox.addEventListener('click', toggleDropdown);
|
||||||
|
filterEfBox.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleDropdown(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filterEfMenu.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadInitialData() {
|
async function loadInitialData() {
|
||||||
try {
|
try {
|
||||||
[educationForms, departments, specialties, academicYears, calendars] = await Promise.all([
|
[educationForms, departments, specialties, academicYears, calendars] = await Promise.all([
|
||||||
@@ -148,13 +187,63 @@ export async function initGroups() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyGroupFilter() {
|
function applyGroupFilter() {
|
||||||
const filterId = filterEfSelect.value;
|
const selectedIds = selectedFilterEducationFormIds();
|
||||||
const filtered = filterId
|
const filtered = selectedIds.length
|
||||||
? allGroups.filter(group => group.educationFormId == filterId)
|
? allGroups.filter(group => selectedIds.includes(String(group.educationFormId)))
|
||||||
: allGroups;
|
: allGroups;
|
||||||
renderGroups(filtered);
|
renderGroups(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFilterEfChange(event) {
|
||||||
|
const changedCheckbox = event.target.closest('input[type="checkbox"]');
|
||||||
|
const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]');
|
||||||
|
|
||||||
|
if (changedCheckbox?.value === 'all' && changedCheckbox.checked) {
|
||||||
|
filterEfCheckboxes.querySelectorAll('input[type="checkbox"]:not([value="all"])').forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
} else if (changedCheckbox?.value !== 'all' && changedCheckbox?.checked) {
|
||||||
|
allCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedFilterEducationFormIds().length) {
|
||||||
|
allCheckbox.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilterEfText();
|
||||||
|
applyGroupFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedFilterEducationFormIds() {
|
||||||
|
const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]');
|
||||||
|
if (allCheckbox?.checked) return [];
|
||||||
|
|
||||||
|
return Array.from(filterEfCheckboxes.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])'))
|
||||||
|
.map(checkbox => String(checkbox.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterEfText() {
|
||||||
|
const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]');
|
||||||
|
const selectedIds = selectedFilterEducationFormIds();
|
||||||
|
if (allCheckbox?.checked) {
|
||||||
|
filterEfText.textContent = 'Все';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
filterEfText.textContent = FILTER_EF_EMPTY_TEXT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIds.length === 1) {
|
||||||
|
const form = educationForms.find(item => String(item.id) === selectedIds[0]);
|
||||||
|
filterEfText.textContent = form?.name || FILTER_EF_EMPTY_TEXT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterEfText.textContent = `Выбрано: ${selectedIds.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGroups(groups) {
|
function renderGroups(groups) {
|
||||||
if (!groups || !groups.length) {
|
if (!groups || !groups.length) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет групп</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет групп</td></tr>';
|
||||||
@@ -188,11 +277,30 @@ export async function initGroups() {
|
|||||||
function populateEfSelects(forms) {
|
function populateEfSelects(forms) {
|
||||||
populateEfSelect(newGroupEfSelect, forms);
|
populateEfSelect(newGroupEfSelect, forms);
|
||||||
populateEfSelect(editGroupEfSelect, forms);
|
populateEfSelect(editGroupEfSelect, forms);
|
||||||
const currentFilter = filterEfSelect.value;
|
renderFilterEfCheckboxes(forms);
|
||||||
filterEfSelect.innerHTML = '<option value="">Все формы</option>' +
|
}
|
||||||
forms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
|
|
||||||
if (currentFilter) filterEfSelect.value = currentFilter;
|
function renderFilterEfCheckboxes(forms) {
|
||||||
syncSelects(filterEfSelect);
|
const selectedIds = new Set(selectedFilterEducationFormIds());
|
||||||
|
const allChecked = !selectedIds.size;
|
||||||
|
filterEfCheckboxes.innerHTML = `
|
||||||
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" value="all" ${allChecked ? 'checked' : ''}>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<span>Все</span>
|
||||||
|
</label>
|
||||||
|
${forms.map(form => {
|
||||||
|
const id = String(form.id);
|
||||||
|
return `
|
||||||
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" value="${escapeHtml(id)}" ${selectedIds.has(id) ? 'checked' : ''}>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<span>${escapeHtml(form.name)}</span>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
`;
|
||||||
|
updateFilterEfText();
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateEfSelect(select, forms) {
|
function populateEfSelect(select, forms) {
|
||||||
@@ -411,6 +519,7 @@ export async function initGroups() {
|
|||||||
|
|
||||||
// --- Управление подгруппами ---
|
// --- Управление подгруппами ---
|
||||||
function openSubgroupsModal(group) {
|
function openSubgroupsModal(group) {
|
||||||
|
activeSubgroupGroup = group;
|
||||||
subgroupsGroupContext.textContent = `Группа: ${group.name} (${group.groupSize} чел.)`;
|
subgroupsGroupContext.textContent = `Группа: ${group.name} (${group.groupSize} чел.)`;
|
||||||
manageSubgroupGroupIdInput.value = group.id;
|
manageSubgroupGroupIdInput.value = group.id;
|
||||||
resetSubgroupForm();
|
resetSubgroupForm();
|
||||||
@@ -424,13 +533,19 @@ export async function initGroups() {
|
|||||||
manageSubgroupsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Подгруппы не созданы</td></tr>';
|
manageSubgroupsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Подгруппы не созданы</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const canDeleteSingleSubgroup = list.length === 1;
|
||||||
|
const deleteTitle = canDeleteSingleSubgroup
|
||||||
|
? 'Удалить подгруппу'
|
||||||
|
: 'Чтобы изменить количество подгрупп, выберите нужный режим выше и нажмите «Применить»';
|
||||||
manageSubgroupsTbody.innerHTML = list.map(sub => `
|
manageSubgroupsTbody.innerHTML = list.map(sub => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(sub.name)}</td>
|
<td>${escapeHtml(sub.name)}</td>
|
||||||
<td>${escapeHtml(String(sub.studentCapacity ?? '-'))} чел.</td>
|
<td>${escapeHtml(String(sub.studentCapacity ?? '-'))} чел.</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-secondary btn-edit-subgroup" data-id="${sub.id}">Изменить</button>
|
<button class="btn btn-sm btn-secondary btn-edit-subgroup" data-id="${sub.id}">Изменить</button>
|
||||||
<button class="btn btn-sm btn-danger btn-delete-subgroup" data-id="${sub.id}">Удалить</button>
|
<button class="btn btn-sm btn-danger btn-delete-subgroup" data-id="${sub.id}"
|
||||||
|
${canDeleteSingleSubgroup ? '' : 'disabled'}
|
||||||
|
title="${escapeHtml(deleteTitle)}">${canDeleteSingleSubgroup ? 'Удалить' : 'Изменить выше'}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -438,46 +553,157 @@ export async function initGroups() {
|
|||||||
|
|
||||||
function resetSubgroupForm() {
|
function resetSubgroupForm() {
|
||||||
manageSubgroupIdInput.value = '';
|
manageSubgroupIdInput.value = '';
|
||||||
manageSubgroupNameInput.value = '';
|
const current = currentGroupSubgroups(manageSubgroupGroupIdInput.value);
|
||||||
manageSubgroupCapacityInput.value = '';
|
const count = [2, 3].includes(current.length) ? current.length : 0;
|
||||||
subgroupFormTitle.textContent = 'Создание подгруппы';
|
manageSubgroupCountSelect.value = String(count);
|
||||||
btnSaveSubgroup.textContent = 'Создать';
|
syncSelects(manageSubgroupCountSelect);
|
||||||
|
renderSubgroupCapacityFields(count, current.map(item => item.studentCapacity));
|
||||||
|
subgroupFormTitle.textContent = 'Настройка подгрупп';
|
||||||
|
btnSaveSubgroup.textContent = 'Применить';
|
||||||
btnCancelSubgroupEdit.style.display = 'none';
|
btnCancelSubgroupEdit.style.display = 'none';
|
||||||
hideAlert('manage-subgroup-alert');
|
hideAlert('manage-subgroup-alert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentGroupSubgroups(groupId) {
|
||||||
|
return subgroups
|
||||||
|
.filter(sub => String(sub.groupId) === String(groupId))
|
||||||
|
.sort((left, right) => naturalSubgroupOrder(left.name).localeCompare(naturalSubgroupOrder(right.name), 'ru'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function naturalSubgroupOrder(name) {
|
||||||
|
const number = String(name || '').match(/\d+/)?.[0];
|
||||||
|
return number ? String(number).padStart(3, '0') : String(name || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubgroupCapacityFields(count, capacities = []) {
|
||||||
|
manageSubgroupCapacities.className = `subgroup-capacity-fields subgroup-capacity-fields-${count}`;
|
||||||
|
if (!count) {
|
||||||
|
manageSubgroupCapacities.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupSize = Number(activeSubgroupGroup?.groupSize || 0);
|
||||||
|
const suggested = suggestedSubgroupCapacities(groupSize, count);
|
||||||
|
manageSubgroupCapacities.innerHTML = Array.from({ length: count }, (_, index) => {
|
||||||
|
const value = capacities[index] ?? suggested[index] ?? '';
|
||||||
|
return `
|
||||||
|
<div class="form-group subgroup-capacity-field">
|
||||||
|
<label for="manage-subgroup-capacity-${index + 1}">Численность ${index + 1}</label>
|
||||||
|
<input type="number" id="manage-subgroup-capacity-${index + 1}" class="manage-subgroup-capacity"
|
||||||
|
min="1" max="${groupSize || ''}" value="${escapeHtml(String(value))}" required>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestedSubgroupCapacities(groupSize, count) {
|
||||||
|
if (!groupSize || !count) return [];
|
||||||
|
const base = Math.floor(groupSize / count);
|
||||||
|
const remainder = groupSize % count;
|
||||||
|
return Array.from({ length: count }, (_, index) => base + (index < remainder ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSubgroup(event) {
|
async function saveSubgroup(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
hideAlert('manage-subgroup-alert');
|
hideAlert('manage-subgroup-alert');
|
||||||
const groupId = manageSubgroupGroupIdInput.value;
|
const groupId = manageSubgroupGroupIdInput.value;
|
||||||
const subgroupId = manageSubgroupIdInput.value;
|
const group = activeSubgroupGroup || allGroups.find(item => String(item.id) === String(groupId));
|
||||||
const name = manageSubgroupNameInput.value.trim();
|
const count = Number(manageSubgroupCountSelect.value);
|
||||||
const capacity = manageSubgroupCapacityInput.value;
|
const existingSubgroups = currentGroupSubgroups(groupId);
|
||||||
|
|
||||||
if (!name || !capacity) {
|
if (!group) {
|
||||||
showAlert('manage-subgroup-alert', 'Заполните все поля', 'error');
|
showAlert('manage-subgroup-alert', 'Группа не найдена', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
if (count === 0) {
|
||||||
name,
|
await applyNoSubgroups(groupId, existingSubgroups);
|
||||||
studentCapacity: Number(capacity)
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const capacities = Array.from(manageSubgroupCapacities.querySelectorAll('.manage-subgroup-capacity'))
|
||||||
|
.map(input => Number(input.value));
|
||||||
|
const validationError = validateSubgroupCapacities(capacities, Number(group.groupSize), count);
|
||||||
|
if (validationError) {
|
||||||
|
showAlert('manage-subgroup-alert', validationError, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const endpoint = `/api/groups/${groupId}/subgroups${subgroupId ? `/${subgroupId}` : ''}`;
|
await syncSubgroups(groupId, existingSubgroups, capacities);
|
||||||
const saved = subgroupId
|
showAlert('manage-subgroup-alert', 'Подгруппы сохранены', 'success');
|
||||||
? await api.put(endpoint, payload)
|
|
||||||
: await api.post(endpoint, payload);
|
|
||||||
showAlert('manage-subgroup-alert', `Подгруппа "${escapeHtml(saved.name)}" сохранена`, 'success');
|
|
||||||
resetSubgroupForm();
|
|
||||||
await loadSubgroups();
|
await loadSubgroups();
|
||||||
|
resetSubgroupForm();
|
||||||
renderSubgroupsList(groupId);
|
renderSubgroupsList(groupId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showAlert('manage-subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error');
|
showAlert('manage-subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateSubgroupCapacities(capacities, groupSize, count) {
|
||||||
|
if (capacities.length !== count || capacities.some(value => !Number.isInteger(value) || value <= 0)) {
|
||||||
|
return 'Укажите положительную численность для каждой подгруппы';
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = capacities.reduce((sum, value) => sum + value, 0);
|
||||||
|
if (total !== groupSize) {
|
||||||
|
return `Сумма численностей подгрупп должна быть равна численности группы (${groupSize} чел.)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyNoSubgroups(groupId, existingSubgroups) {
|
||||||
|
try {
|
||||||
|
for (const subgroup of existingSubgroups) {
|
||||||
|
await api.delete(`/api/groups/${groupId}/subgroups/${subgroup.id}`);
|
||||||
|
}
|
||||||
|
showAlert('manage-subgroup-alert', 'Деление на подгруппы отключено', 'success');
|
||||||
|
await loadSubgroups();
|
||||||
|
resetSubgroupForm();
|
||||||
|
renderSubgroupsList(groupId);
|
||||||
|
} catch (error) {
|
||||||
|
showAlert('manage-subgroup-alert', error.message || 'Ошибка отключения подгрупп', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncSubgroups(groupId, existingSubgroups, capacities) {
|
||||||
|
const targetCount = capacities.length;
|
||||||
|
const extraSubgroups = existingSubgroups.slice(targetCount);
|
||||||
|
for (const subgroup of extraSubgroups) {
|
||||||
|
await api.delete(`/api/groups/${groupId}/subgroups/${subgroup.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = [];
|
||||||
|
const creates = [];
|
||||||
|
for (let index = 0; index < targetCount; index += 1) {
|
||||||
|
const payload = {
|
||||||
|
name: `Подгруппа ${index + 1}`,
|
||||||
|
studentCapacity: capacities[index]
|
||||||
|
};
|
||||||
|
const existing = existingSubgroups[index];
|
||||||
|
if (existing) {
|
||||||
|
updates.push({ existing, payload });
|
||||||
|
} else {
|
||||||
|
creates.push(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.sort((left, right) => {
|
||||||
|
const leftIncreases = left.payload.studentCapacity > Number(left.existing.studentCapacity || 0);
|
||||||
|
const rightIncreases = right.payload.studentCapacity > Number(right.existing.studentCapacity || 0);
|
||||||
|
return Number(leftIncreases) - Number(rightIncreases);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
await api.put(`/api/groups/${groupId}/subgroups/${update.existing.id}`, update.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const payload of creates) {
|
||||||
|
await api.post(`/api/groups/${groupId}/subgroups`, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubgroupTableClick(event) {
|
async function handleSubgroupTableClick(event) {
|
||||||
const editButton = event.target.closest('.btn-edit-subgroup');
|
const editButton = event.target.closest('.btn-edit-subgroup');
|
||||||
const deleteButton = event.target.closest('.btn-delete-subgroup');
|
const deleteButton = event.target.closest('.btn-delete-subgroup');
|
||||||
@@ -487,17 +713,19 @@ export async function initGroups() {
|
|||||||
const subgroup = subgroups.find(item => item.id == editButton.dataset.id);
|
const subgroup = subgroups.find(item => item.id == editButton.dataset.id);
|
||||||
if (!subgroup) return;
|
if (!subgroup) return;
|
||||||
|
|
||||||
manageSubgroupIdInput.value = subgroup.id;
|
resetSubgroupForm();
|
||||||
manageSubgroupNameInput.value = subgroup.name || '';
|
const inputIndex = currentGroupSubgroups(groupId).findIndex(item => item.id === subgroup.id);
|
||||||
manageSubgroupCapacityInput.value = subgroup.studentCapacity ?? '';
|
const input = manageSubgroupCapacities.querySelectorAll('.manage-subgroup-capacity')[inputIndex];
|
||||||
subgroupFormTitle.textContent = 'Редактирование подгруппы';
|
input?.focus();
|
||||||
btnSaveSubgroup.textContent = 'Сохранить';
|
|
||||||
btnCancelSubgroupEdit.style.display = 'inline-block';
|
|
||||||
hideAlert('manage-subgroup-alert');
|
hideAlert('manage-subgroup-alert');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteButton) {
|
if (deleteButton) {
|
||||||
|
if (deleteButton.disabled) {
|
||||||
|
showAlert('manage-subgroup-alert', 'Чтобы изменить количество подгрупп, выберите нужный режим выше и нажмите «Применить»', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!confirm('Удалить подгруппу?')) return;
|
if (!confirm('Удалить подгруппу?')) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/api/groups/${groupId}/subgroups/${deleteButton.dataset.id}`);
|
await api.delete(`/api/groups/${groupId}/subgroups/${deleteButton.dataset.id}`);
|
||||||
|
|||||||
@@ -49,10 +49,26 @@
|
|||||||
<div class="card-header-row">
|
<div class="card-header-row">
|
||||||
<h2>Все группы</h2>
|
<h2>Все группы</h2>
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<label for="filter-ef">Фильтр:</label>
|
<label for="filter-ef-box">Фильтр:</label>
|
||||||
<select id="filter-ef">
|
<div class="custom-multi-select">
|
||||||
<option value="">Все формы</option>
|
<div class="select-box" id="filter-ef-box" role="button" tabindex="0" aria-expanded="false">
|
||||||
</select>
|
<span class="select-text" id="filter-ef-text">Выберите форму обучения</span>
|
||||||
|
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu" id="filter-ef-menu">
|
||||||
|
<div id="filter-ef-checkboxes" class="checkbox-group-vertical">
|
||||||
|
<label class="checkbox-item">
|
||||||
|
<input type="checkbox" value="all" checked>
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<span>Все</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -152,17 +168,18 @@
|
|||||||
<form id="manage-subgroup-form">
|
<form id="manage-subgroup-form">
|
||||||
<input type="hidden" id="manage-subgroup-id">
|
<input type="hidden" id="manage-subgroup-id">
|
||||||
<input type="hidden" id="manage-subgroup-group-id">
|
<input type="hidden" id="manage-subgroup-group-id">
|
||||||
<div class="form-row" style="align-items: flex-end;">
|
<div class="subgroup-config-grid">
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div class="form-group subgroup-count-field">
|
||||||
<label for="manage-subgroup-name">Название подгруппы</label>
|
<label for="manage-subgroup-count">Количество подгрупп</label>
|
||||||
<input type="text" id="manage-subgroup-name" placeholder="Подгруппа 1" required>
|
<select id="manage-subgroup-count">
|
||||||
|
<option value="0">Без разделения на подгруппы</option>
|
||||||
|
<option value="2">Две подгруппы</option>
|
||||||
|
<option value="3">Три подгруппы</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div id="manage-subgroup-capacities" class="subgroup-capacity-fields"></div>
|
||||||
<label for="manage-subgroup-capacity">Численность (чел.)</label>
|
<div class="subgroup-actions">
|
||||||
<input type="number" id="manage-subgroup-capacity" min="1" placeholder="12" required>
|
<button type="submit" class="btn btn-md btn-primary" id="btn-save-subgroup">Применить</button>
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-bottom: 0;">
|
|
||||||
<button type="submit" class="btn btn-md btn-primary" id="btn-save-subgroup">Создать</button>
|
|
||||||
<button type="button" class="btn btn-md btn-ghost" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button>
|
<button type="button" class="btn btn-md btn-ghost" id="btn-cancel-subgroup-edit" style="display: none;">Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user