написал обработку количества часов лекций лабораторных и практик по отдельности
This commit is contained in:
@@ -124,6 +124,33 @@
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.schedule-hours-panel {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.schedule-hours-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(220px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.schedule-hours-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.65rem;
|
||||
align-items: end;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.schedule-hours-title {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.schedule-slots-panel {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -448,6 +475,10 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.schedule-hours-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.schedule-slot-row {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
@@ -464,6 +495,10 @@
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.schedule-hours-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.schedule-slot-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -22,14 +22,43 @@ const SEMESTER_LABELS = {
|
||||
spring: 'весенний'
|
||||
};
|
||||
|
||||
const LESSON_TYPE_LIMITS = [
|
||||
{
|
||||
key: 'lecture',
|
||||
label: 'Лекции',
|
||||
hoursField: 'lectureAcademicHours',
|
||||
startWeekField: 'lectureStartWeek',
|
||||
hoursInputId: 'schedule-rule-lecture-hours',
|
||||
startWeekInputId: 'schedule-rule-lecture-start-week'
|
||||
},
|
||||
{
|
||||
key: 'laboratory',
|
||||
label: 'Лабораторные',
|
||||
hoursField: 'laboratoryAcademicHours',
|
||||
startWeekField: 'laboratoryStartWeek',
|
||||
hoursInputId: 'schedule-rule-laboratory-hours',
|
||||
startWeekInputId: 'schedule-rule-laboratory-start-week'
|
||||
},
|
||||
{
|
||||
key: 'practice',
|
||||
label: 'Практики',
|
||||
hoursField: 'practiceAcademicHours',
|
||||
startWeekField: 'practiceStartWeek',
|
||||
hoursInputId: 'schedule-rule-practice-hours',
|
||||
startWeekInputId: 'schedule-rule-practice-start-week'
|
||||
}
|
||||
];
|
||||
|
||||
export async function initSchedule() {
|
||||
const ruleForm = document.getElementById('schedule-rule-form');
|
||||
const ruleFormTitle = document.getElementById('schedule-rule-form-title');
|
||||
const ruleIdInput = document.getElementById('schedule-rule-id');
|
||||
const ruleSubjectSelect = document.getElementById('schedule-rule-subject');
|
||||
const ruleSemesterSelect = document.getElementById('schedule-rule-semester');
|
||||
const ruleActiveFromInput = document.getElementById('schedule-rule-active-from');
|
||||
const ruleHoursInput = document.getElementById('schedule-rule-hours');
|
||||
const ruleTypeInputs = Object.fromEntries(LESSON_TYPE_LIMITS.map(config => [config.key, {
|
||||
hours: document.getElementById(config.hoursInputId),
|
||||
startWeek: document.getElementById(config.startWeekInputId)
|
||||
}]));
|
||||
const ruleGroupsContainer = document.getElementById('schedule-rule-groups');
|
||||
const ruleSlotsContainer = document.getElementById('schedule-rule-slots');
|
||||
const ruleResetButton = document.getElementById('schedule-rule-reset');
|
||||
@@ -92,18 +121,18 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Загрузка...</td></tr>';
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
rules = await api.get('/api/admin/schedule-rules');
|
||||
renderRules();
|
||||
} catch (error) {
|
||||
rulesTbody.innerHTML = `<tr><td colspan="8" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
rulesTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderRules() {
|
||||
if (!rules.length) {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Правила расписания не созданы</td></tr>';
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Правила расписания не созданы</td></tr>';
|
||||
return;
|
||||
}
|
||||
rulesTbody.innerHTML = rules.map(rule => `
|
||||
@@ -112,8 +141,7 @@ export async function initSchedule() {
|
||||
<td>${escapeHtml(rule.subjectName || '-')}</td>
|
||||
<td>${escapeHtml(formatSemester(rule))}</td>
|
||||
<td>${escapeHtml((rule.groupNames || []).join(', ') || '-')}</td>
|
||||
<td>${escapeHtml(rule.activeFromDate || '-')}</td>
|
||||
<td>${escapeHtml(String(rule.totalAcademicHours ?? '-'))}</td>
|
||||
<td>${renderRuleLoadSummary(rule)}</td>
|
||||
<td>${renderRuleSlotsSummary(rule.slots)}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-rule" data-id="${rule.id}">Изменить</button>
|
||||
@@ -140,6 +168,20 @@ export async function initSchedule() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderRuleLoadSummary(rule) {
|
||||
const rows = LESSON_TYPE_LIMITS
|
||||
.map(config => ({
|
||||
label: config.label,
|
||||
hours: Number(rule[config.hoursField] || 0),
|
||||
startWeek: Number(rule[config.startWeekField] || 1)
|
||||
}))
|
||||
.filter(item => item.hours > 0);
|
||||
if (!rows.length) return '<span class="muted">Часы не указаны</span>';
|
||||
return rows.map(item => (
|
||||
`<div class="schedule-slot-summary">${escapeHtml(`${item.label}: ${item.hours} ч, с ${item.startWeek} недели`)}</div>`
|
||||
)).join('');
|
||||
}
|
||||
|
||||
async function handleRuleTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-rule');
|
||||
const deleteButton = event.target.closest('.btn-delete-rule');
|
||||
@@ -186,16 +228,15 @@ export async function initSchedule() {
|
||||
const slots = readRuleSlots(true);
|
||||
if (!ruleSubjectSelect.value) throw new Error('Выберите дисциплину');
|
||||
if (!ruleSemesterSelect.value) throw new Error('Выберите семестр');
|
||||
if (!ruleActiveFromInput.value) throw new Error('Укажите дату начала правила');
|
||||
if (!ruleHoursInput.value || Number(ruleHoursInput.value) <= 0) throw new Error('Укажите количество академических часов');
|
||||
const typeLimits = readTypeLimits();
|
||||
validateTypeLimits(typeLimits, slots);
|
||||
if (!groupIds.length) throw new Error('Выберите хотя бы одну группу');
|
||||
if (!slots.length) throw new Error('Добавьте хотя бы один слот занятия');
|
||||
return {
|
||||
id: ruleIdInput.value ? Number(ruleIdInput.value) : null,
|
||||
subjectId: Number(ruleSubjectSelect.value),
|
||||
semesterId: Number(ruleSemesterSelect.value),
|
||||
activeFromDate: ruleActiveFromInput.value,
|
||||
totalAcademicHours: Number(ruleHoursInput.value),
|
||||
...typeLimits,
|
||||
groupIds,
|
||||
slots
|
||||
};
|
||||
@@ -206,8 +247,7 @@ export async function initSchedule() {
|
||||
ruleIdInput.value = rule.id;
|
||||
ruleSubjectSelect.value = rule.subjectId || '';
|
||||
ruleSemesterSelect.value = rule.semesterId || '';
|
||||
ruleActiveFromInput.value = rule.activeFromDate || '';
|
||||
ruleHoursInput.value = rule.totalAcademicHours || '';
|
||||
fillTypeLimits(rule);
|
||||
syncSelects(ruleSubjectSelect, ruleSemesterSelect);
|
||||
renderRuleGroups(rule.groupIds || []);
|
||||
renderRuleSlots(rule.slots && rule.slots.length ? rule.slots : [{}]);
|
||||
@@ -219,6 +259,7 @@ export async function initSchedule() {
|
||||
ruleForm.reset();
|
||||
ruleFormTitle.textContent = 'Новое правило расписания';
|
||||
ruleIdInput.value = '';
|
||||
fillTypeLimits({});
|
||||
renderRuleGroups([]);
|
||||
renderRuleSlots([{}]);
|
||||
syncSelects(ruleSubjectSelect, ruleSemesterSelect);
|
||||
@@ -277,6 +318,42 @@ export async function initSchedule() {
|
||||
});
|
||||
}
|
||||
|
||||
function readTypeLimits() {
|
||||
return LESSON_TYPE_LIMITS.reduce((payload, config) => {
|
||||
const inputs = ruleTypeInputs[config.key];
|
||||
payload[config.hoursField] = Number(inputs.hours.value || 0);
|
||||
payload[config.startWeekField] = Number(inputs.startWeek.value || 1);
|
||||
return payload;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function fillTypeLimits(rule) {
|
||||
LESSON_TYPE_LIMITS.forEach(config => {
|
||||
const inputs = ruleTypeInputs[config.key];
|
||||
inputs.hours.value = rule[config.hoursField] ?? 0;
|
||||
inputs.startWeek.value = rule[config.startWeekField] ?? 1;
|
||||
});
|
||||
}
|
||||
|
||||
function validateTypeLimits(typeLimits, slots) {
|
||||
const selectedTypeKeys = new Set(slots.map(slot => lessonTypeKeyById(slot.lessonTypeId)).filter(Boolean));
|
||||
let hasHours = false;
|
||||
for (const config of LESSON_TYPE_LIMITS) {
|
||||
const hours = typeLimits[config.hoursField];
|
||||
const startWeek = typeLimits[config.startWeekField];
|
||||
if (hours < 0) throw new Error('Количество часов по типам занятий не может быть отрицательным');
|
||||
if (!startWeek || startWeek <= 0) throw new Error('Неделя начала занятий должна быть больше нуля');
|
||||
if (hours > 0) hasHours = true;
|
||||
if (hours > 0 && !selectedTypeKeys.has(config.key)) {
|
||||
throw new Error(`Для типа "${config.label}" добавьте хотя бы один слот`);
|
||||
}
|
||||
if (hours === 0 && selectedTypeKeys.has(config.key)) {
|
||||
throw new Error(`Для типа "${config.label}" укажите часы или удалите слоты этого типа`);
|
||||
}
|
||||
}
|
||||
if (!hasHours) throw new Error('Укажите часы хотя бы для одного типа занятий');
|
||||
}
|
||||
|
||||
async function loadTimeSlots() {
|
||||
const slots = await api.get('/api/admin/time-slots');
|
||||
timeSlots = slots
|
||||
@@ -322,6 +399,19 @@ export async function initSchedule() {
|
||||
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
|
||||
}
|
||||
|
||||
function lessonTypeKeyById(lessonTypeId) {
|
||||
const lessonType = lessonTypes.find(item => String(item.id) === String(lessonTypeId));
|
||||
return lessonType ? lessonTypeKey(lessonType) : null;
|
||||
}
|
||||
|
||||
function lessonTypeKey(lessonType) {
|
||||
const name = String(lessonType.name || lessonType.lessonType || '').toLowerCase();
|
||||
if (name.includes('лекц')) return 'lecture';
|
||||
if (name.includes('лаб')) return 'laboratory';
|
||||
if (name.includes('практ')) return 'practice';
|
||||
return null;
|
||||
}
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;
|
||||
|
||||
@@ -18,13 +18,44 @@
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-active-from">Дата начала</label>
|
||||
<input type="date" id="schedule-rule-active-from" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-hours">Академические часы</label>
|
||||
<input type="number" id="schedule-rule-hours" min="1" step="1" placeholder="72" required>
|
||||
</div>
|
||||
|
||||
<div class="schedule-hours-panel">
|
||||
<label class="schedule-section-label">Часы и недели начала</label>
|
||||
<div class="schedule-hours-grid">
|
||||
<div class="schedule-hours-row">
|
||||
<div class="schedule-hours-title">Лекции</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-lecture-hours">Часы</label>
|
||||
<input type="number" id="schedule-rule-lecture-hours" min="0" step="1" value="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-lecture-start-week">Неделя начала</label>
|
||||
<input type="number" id="schedule-rule-lecture-start-week" min="1" step="1" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-hours-row">
|
||||
<div class="schedule-hours-title">Лабораторные</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-laboratory-hours">Часы</label>
|
||||
<input type="number" id="schedule-rule-laboratory-hours" min="0" step="1" value="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-laboratory-start-week">Неделя начала</label>
|
||||
<input type="number" id="schedule-rule-laboratory-start-week" min="1" step="1" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-hours-row">
|
||||
<div class="schedule-hours-title">Практики</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-practice-hours">Часы</label>
|
||||
<input type="number" id="schedule-rule-practice-hours" min="0" step="1" value="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-rule-practice-start-week">Неделя начала</label>
|
||||
<input type="number" id="schedule-rule-practice-start-week" min="1" step="1" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,15 +92,14 @@
|
||||
<th>Дисциплина</th>
|
||||
<th>Семестр</th>
|
||||
<th>Группы</th>
|
||||
<th>Начало</th>
|
||||
<th>Часы</th>
|
||||
<th>Нагрузка</th>
|
||||
<th>Слоты</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedule-tbody">
|
||||
<tr>
|
||||
<td colspan="8" class="loading-row">Загрузка...</td>
|
||||
<td colspan="7" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user