3 Задача
This commit is contained in:
@@ -17,8 +17,15 @@ export async function initTeacherAbsences() {
|
||||
const registry = document.getElementById('teacher-absence-registry');
|
||||
const overlay = document.getElementById('absence-wizard-overlay');
|
||||
const wizardLessons = document.getElementById('absence-wizard-lessons');
|
||||
const preferenceSemesterFilter = document.getElementById('teacher-preference-semester-filter');
|
||||
const preferenceStatusFilter = document.getElementById('teacher-preference-status-filter');
|
||||
const preferenceReviewList = document.getElementById('teacher-preference-review-list');
|
||||
const changeStatusFilter = document.getElementById('teacher-change-status-filter');
|
||||
const changeReviewList = document.getElementById('teacher-change-review-list');
|
||||
let absences = [];
|
||||
let currentWizard = null;
|
||||
let preferenceItems = [];
|
||||
let changeItems = [];
|
||||
|
||||
setDefaultPeriod(startInput, endInput);
|
||||
form?.addEventListener('submit', createAbsence);
|
||||
@@ -31,8 +38,15 @@ export async function initTeacherAbsences() {
|
||||
if (event.target === overlay) closeWizard();
|
||||
});
|
||||
registry?.addEventListener('click', handleRegistryAction);
|
||||
preferenceSemesterFilter?.addEventListener('change', loadPreferenceReviews);
|
||||
preferenceStatusFilter?.addEventListener('change', loadPreferenceReviews);
|
||||
document.getElementById('teacher-preference-refresh')?.addEventListener('click', loadPreferenceReviews);
|
||||
preferenceReviewList?.addEventListener('click', handlePreferenceReview);
|
||||
changeStatusFilter?.addEventListener('change', loadChangeReviews);
|
||||
document.getElementById('teacher-change-refresh')?.addEventListener('click', loadChangeReviews);
|
||||
changeReviewList?.addEventListener('click', handleChangeReview);
|
||||
|
||||
await Promise.all([loadTeachers(), loadAbsences()]);
|
||||
await Promise.all([loadTeachers(), loadAbsences(), loadPreferenceMeta(), loadChangeReviews()]);
|
||||
|
||||
async function loadTeachers() {
|
||||
try {
|
||||
@@ -249,6 +263,152 @@ export async function initTeacherAbsences() {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreferenceMeta() {
|
||||
preferenceReviewList.innerHTML = '<div class="loading-row">Загрузка семестров...</div>';
|
||||
try {
|
||||
const meta = await api.get('/api/teacher-preferences/meta');
|
||||
preferenceSemesterFilter.innerHTML = '<option value="">Все семестры</option>' + (meta.semesters || []).map(item => `
|
||||
<option value="${item.id}">${escapeHtml(semesterLabel(item))}</option>
|
||||
`).join('');
|
||||
const today = isoDate(new Date());
|
||||
const current = (meta.semesters || []).find(item => item.startDate <= today && item.endDate >= today);
|
||||
if (current) preferenceSemesterFilter.value = String(current.id);
|
||||
await loadPreferenceReviews();
|
||||
} catch (error) {
|
||||
preferenceReviewList.innerHTML = `<div class="loading-row">${escapeHtml(error.message || 'Не удалось загрузить семестры')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreferenceReviews() {
|
||||
preferenceReviewList.innerHTML = '<div class="loading-row">Загрузка пожеланий...</div>';
|
||||
const params = new URLSearchParams();
|
||||
if (preferenceSemesterFilter.value) params.set('semesterId', preferenceSemesterFilter.value);
|
||||
if (preferenceStatusFilter.value) params.set('status', preferenceStatusFilter.value);
|
||||
try {
|
||||
preferenceItems = await api.get(`/api/teacher-preferences${params.size ? `?${params}` : ''}`);
|
||||
document.getElementById('teacher-preference-pending-count').textContent = String(
|
||||
preferenceItems.filter(item => item.status === 'PENDING').length
|
||||
);
|
||||
renderPreferenceReviews();
|
||||
} catch (error) {
|
||||
preferenceReviewList.innerHTML = `<div class="loading-row">${escapeHtml(error.message || 'Не удалось загрузить пожелания')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreferenceReviews() {
|
||||
preferenceReviewList.innerHTML = preferenceItems.length ? preferenceItems.map(item => `
|
||||
<article class="teacher-dialogue-item">
|
||||
<span class="teacher-dialogue-mark preference-${escapeHtml(item.preferenceType.toLowerCase())}"></span>
|
||||
<div class="teacher-dialogue-copy">
|
||||
<small>${escapeHtml(item.academicYearTitle)} · ${escapeHtml(item.semesterType === 'AUTUMN' ? 'осенний семестр' : 'весенний семестр')}</small>
|
||||
<h3>${escapeHtml(item.teacherName)}</h3>
|
||||
<p>${escapeHtml(preferenceDescription(item))}</p>
|
||||
${item.comment ? `<blockquote>${escapeHtml(item.comment)}</blockquote>` : ''}
|
||||
${item.reviewComment ? `<span class="teacher-dialogue-review">Решение: ${escapeHtml(item.reviewComment)}</span>` : ''}
|
||||
</div>
|
||||
<div class="teacher-dialogue-action">
|
||||
<span class="absence-status ${statusClass(item.status)}">${escapeHtml(genericStatusLabel(item.status))}</span>
|
||||
${item.status === 'PENDING' ? `
|
||||
<input type="text" class="teacher-dialogue-comment" maxlength="2000" placeholder="Комментарий к решению">
|
||||
<div>
|
||||
<button class="btn btn-sm btn-primary" type="button" data-preference-review="approve" data-id="${item.id}">Согласовать</button>
|
||||
<button class="btn btn-sm btn-danger-subtle" type="button" data-preference-review="reject" data-id="${item.id}">Отклонить</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</article>
|
||||
`).join('') : '<div class="absence-empty"><strong>Пожеланий не найдено</strong><span>Измените фильтры или дождитесь новых отметок преподавателей.</span></div>';
|
||||
}
|
||||
|
||||
async function handlePreferenceReview(event) {
|
||||
const button = event.target.closest('[data-preference-review][data-id]');
|
||||
if (!button) return;
|
||||
const card = button.closest('.teacher-dialogue-item');
|
||||
const approved = button.dataset.preferenceReview === 'approve';
|
||||
const comment = card.querySelector('.teacher-dialogue-comment')?.value.trim() || '';
|
||||
if (!approved && !comment) {
|
||||
window.alert('При отклонении укажите комментарий');
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api.post(`/api/teacher-preferences/${button.dataset.id}/review`, {
|
||||
approved,
|
||||
comment: comment || 'Согласовано ответственным сотрудником'
|
||||
});
|
||||
await loadPreferenceReviews();
|
||||
} catch (error) {
|
||||
window.alert(error.message || 'Не удалось рассмотреть пожелание');
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChangeReviews() {
|
||||
changeReviewList.innerHTML = '<div class="loading-row">Загрузка заявок...</div>';
|
||||
const params = changeStatusFilter.value ? `?status=${encodeURIComponent(changeStatusFilter.value)}` : '';
|
||||
try {
|
||||
changeItems = await api.get(`/api/teacher-change-requests${params}`);
|
||||
document.getElementById('teacher-change-pending-count').textContent = String(
|
||||
changeItems.filter(item => item.status === 'PENDING').length
|
||||
);
|
||||
renderChangeReviews();
|
||||
} catch (error) {
|
||||
changeReviewList.innerHTML = `<div class="loading-row">${escapeHtml(error.message || 'Не удалось загрузить заявки')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderChangeReviews() {
|
||||
const canApply = role === 'ADMIN' || role === 'EDUCATION_OFFICE';
|
||||
changeReviewList.innerHTML = changeItems.length ? changeItems.map(item => {
|
||||
const target = item.requestType === 'MOVE'
|
||||
? `${formatDate(item.targetLessonDate || item.lessonDate)} · ${trimTime(item.requestedStartTime)}–${trimTime(item.requestedEndTime)}`
|
||||
: item.requestType === 'CHANGE_CLASSROOM' ? item.requestedClassroomName : 'Отмена занятия';
|
||||
return `<article class="teacher-dialogue-item change-item">
|
||||
<span class="teacher-dialogue-mark change-${escapeHtml(item.requestType.toLowerCase())}"></span>
|
||||
<div class="teacher-dialogue-copy">
|
||||
<small>${formatDate(item.lessonDate)} · ${trimTime(item.sourceStartTime)}–${trimTime(item.sourceEndTime)} · ${escapeHtml(item.sourceClassroomName)}</small>
|
||||
<h3>${escapeHtml(item.teacherName)} · ${escapeHtml(item.subjectName)}</h3>
|
||||
<p>${escapeHtml(changeTypeLabel(item.requestType))} → <strong>${escapeHtml(target)}</strong></p>
|
||||
<blockquote>${escapeHtml(item.reason)}</blockquote>
|
||||
${item.reviewComment ? `<span class="teacher-dialogue-review">Решение: ${escapeHtml(item.reviewComment)}</span>` : ''}
|
||||
<details><summary>История · ${(item.history || []).length}</summary>
|
||||
<ol>${(item.history || []).map(entry => `<li><b>${genericStatusLabel(entry.status)}</b> · ${escapeHtml(entry.comment || 'Без комментария')}</li>`).join('')}</ol>
|
||||
</details>
|
||||
</div>
|
||||
<div class="teacher-dialogue-action">
|
||||
<span class="absence-status ${statusClass(item.status)}">${escapeHtml(genericStatusLabel(item.status))}</span>
|
||||
${item.status === 'PENDING' && canApply ? `
|
||||
<input type="text" class="teacher-dialogue-comment" maxlength="2000" placeholder="Комментарий к решению">
|
||||
<div>
|
||||
<button class="btn btn-sm btn-primary" type="button" data-change-review="approve" data-id="${item.id}">Применить</button>
|
||||
<button class="btn btn-sm btn-danger-subtle" type="button" data-change-review="reject" data-id="${item.id}">Отклонить</button>
|
||||
</div>
|
||||
` : item.status === 'PENDING' ? '<small>Применение доступно учебному отделу</small>' : ''}
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('') : '<div class="absence-empty"><strong>Заявок не найдено</strong><span>Очередь изменений сейчас пуста.</span></div>';
|
||||
}
|
||||
|
||||
async function handleChangeReview(event) {
|
||||
const button = event.target.closest('[data-change-review][data-id]');
|
||||
if (!button) return;
|
||||
const card = button.closest('.teacher-dialogue-item');
|
||||
const approved = button.dataset.changeReview === 'approve';
|
||||
const comment = card.querySelector('.teacher-dialogue-comment')?.value.trim() || '';
|
||||
if (!approved && !comment) {
|
||||
window.alert('При отклонении укажите комментарий');
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api.post(`/api/teacher-change-requests/${button.dataset.id}/review`, { approved, comment });
|
||||
await Promise.all([loadChangeReviews(), loadAbsences()]);
|
||||
} catch (error) {
|
||||
window.alert(error.message || 'Не удалось применить решение');
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function decisionOptions(lesson) {
|
||||
@@ -343,6 +503,41 @@ function statusClass(status) {
|
||||
return `is-${String(status || '').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function genericStatusLabel(status) {
|
||||
return {
|
||||
PENDING: 'На рассмотрении',
|
||||
APPROVED: 'Согласовано',
|
||||
REJECTED: 'Отклонено',
|
||||
CANCELLED: 'Отменено'
|
||||
}[status] || status || '—';
|
||||
}
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = semester.semesterType === 'AUTUMN' ? 'осенний' : semester.semesterType === 'SPRING' ? 'весенний' : semester.semesterType;
|
||||
return `${semester.academicYearTitle} · ${type} · ${formatDate(semester.startDate)}–${formatDate(semester.endDate)}`;
|
||||
}
|
||||
|
||||
function preferenceDescription(item) {
|
||||
const type = {
|
||||
HARD_UNAVAILABLE: 'Строгая недоступность',
|
||||
SOFT_PREFERRED: 'Предпочтительное время',
|
||||
SOFT_UNWANTED: 'Нежелательное время',
|
||||
CONSECUTIVE: 'Пожелание «пары подряд»',
|
||||
NO_GAPS: 'Пожелание «без окон»'
|
||||
}[item.preferenceType] || item.preferenceType;
|
||||
if (item.preferenceDate) return `${type}: ${formatDate(item.preferenceDate)}`;
|
||||
if (item.dayName) return `${type}: ${item.dayName}, ${trimTime(item.startTime)}–${trimTime(item.endTime)}`;
|
||||
return type;
|
||||
}
|
||||
|
||||
function changeTypeLabel(type) {
|
||||
return {
|
||||
MOVE: 'Перенос даты или времени',
|
||||
CHANGE_CLASSROOM: 'Смена аудитории',
|
||||
CANCEL: 'Отмена занятия'
|
||||
}[type] || type || 'Изменение';
|
||||
}
|
||||
|
||||
function resolutionLabel(resolution) {
|
||||
return {
|
||||
REPLACE_TEACHER: 'Замена преподавателя',
|
||||
|
||||
Reference in New Issue
Block a user