3 Задача
This commit is contained in:
@@ -22,6 +22,29 @@ import {
|
||||
const absenceStart = document.getElementById('teacher-absence-start');
|
||||
const absenceEnd = document.getElementById('teacher-absence-end');
|
||||
const absenceReason = document.getElementById('teacher-absence-reason');
|
||||
const preferenceSemester = document.getElementById('preference-semester');
|
||||
const preferenceGrid = document.getElementById('preference-grid');
|
||||
const preferenceMessage = document.getElementById('preference-message');
|
||||
const preferenceDateForm = document.getElementById('preference-date-form');
|
||||
const preferenceDate = document.getElementById('preference-date');
|
||||
const preferenceDateComment = document.getElementById('preference-date-comment');
|
||||
const preferenceDateList = document.getElementById('preference-date-list');
|
||||
const preferenceConsecutive = document.getElementById('preference-consecutive');
|
||||
const preferenceNoGaps = document.getElementById('preference-no-gaps');
|
||||
const requestList = document.getElementById('change-request-list');
|
||||
const requestOverlay = document.getElementById('request-dialog-overlay');
|
||||
const requestForm = document.getElementById('change-request-form');
|
||||
const requestType = document.getElementById('change-request-type');
|
||||
const requestDate = document.getElementById('change-request-date');
|
||||
const requestTime = document.getElementById('change-request-time');
|
||||
const requestClassroom = document.getElementById('change-request-classroom');
|
||||
const requestReason = document.getElementById('change-request-reason');
|
||||
const requestMessage = document.getElementById('request-dialog-message');
|
||||
let preferenceMeta = { semesters: [], timeSlots: [] };
|
||||
let preferences = [];
|
||||
let scheduleLessons = new Map();
|
||||
let activeRequestLesson = null;
|
||||
let activeCandidates = null;
|
||||
let weekStart = startOfWeek(new Date());
|
||||
|
||||
document.getElementById('logout-link').addEventListener('click', async (event) => {
|
||||
@@ -58,6 +81,29 @@ import {
|
||||
});
|
||||
absenceForm.addEventListener('submit', submitAbsence);
|
||||
absenceList.addEventListener('click', cancelAbsence);
|
||||
document.querySelectorAll('[data-workspace-tab]').forEach(button => {
|
||||
button.addEventListener('click', () => switchWorkspace(button.dataset.workspaceTab));
|
||||
});
|
||||
preferenceSemester.addEventListener('change', loadPreferences);
|
||||
preferenceGrid.addEventListener('click', handlePreferenceCell);
|
||||
preferenceDateForm.addEventListener('submit', submitUnavailableDate);
|
||||
preferenceDateList.addEventListener('click', cancelPreference);
|
||||
preferenceConsecutive.addEventListener('change', () => toggleCompactPreference('CONSECUTIVE', preferenceConsecutive));
|
||||
preferenceNoGaps.addEventListener('change', () => toggleCompactPreference('NO_GAPS', preferenceNoGaps));
|
||||
grid.addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-request-lesson]');
|
||||
if (button) openRequestDialog(button.dataset.requestLesson);
|
||||
});
|
||||
document.getElementById('change-request-refresh').addEventListener('click', loadChangeRequests);
|
||||
requestList.addEventListener('click', cancelChangeRequest);
|
||||
document.getElementById('request-dialog-close').addEventListener('click', closeRequestDialog);
|
||||
document.getElementById('request-dialog-cancel').addEventListener('click', closeRequestDialog);
|
||||
requestOverlay.addEventListener('click', event => {
|
||||
if (event.target === requestOverlay) closeRequestDialog();
|
||||
});
|
||||
requestType.addEventListener('change', updateRequestFields);
|
||||
requestDate.addEventListener('change', reloadRequestCandidates);
|
||||
requestForm.addEventListener('submit', submitChangeRequest);
|
||||
|
||||
const todayIso = toIsoDate(new Date());
|
||||
absenceStart.value = todayIso;
|
||||
@@ -65,6 +111,351 @@ import {
|
||||
|
||||
loadSchedule();
|
||||
loadAbsences();
|
||||
loadPreferenceMeta();
|
||||
loadChangeRequests();
|
||||
|
||||
function switchWorkspace(name) {
|
||||
document.querySelectorAll('[data-workspace-tab]').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.workspaceTab === name);
|
||||
});
|
||||
document.querySelectorAll('[data-workspace-view]').forEach(view => {
|
||||
const active = view.dataset.workspaceView === name;
|
||||
view.hidden = !active;
|
||||
view.classList.toggle('active', active);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPreferenceMeta() {
|
||||
try {
|
||||
preferenceMeta = await fetchJson('/api/teacher-preferences/meta');
|
||||
const semesters = preferenceMeta.semesters || [];
|
||||
preferenceSemester.innerHTML = semesters.map(semester => `
|
||||
<option value="${semester.id}">${escapeHtml(semesterLabel(semester))}</option>
|
||||
`).join('');
|
||||
const today = toIsoDate(new Date());
|
||||
const current = semesters.find(semester => semester.startDate <= today && semester.endDate >= today);
|
||||
if (current) preferenceSemester.value = String(current.id);
|
||||
await loadPreferences();
|
||||
} catch (error) {
|
||||
showPreferenceMessage(error.message, true);
|
||||
preferenceGrid.innerHTML = '<div class="preference-empty">Не удалось загрузить параметры семестра</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreferences() {
|
||||
if (!preferenceSemester.value) return;
|
||||
preferenceGrid.innerHTML = '<div class="preference-empty">Загрузка календаря...</div>';
|
||||
try {
|
||||
preferences = await fetchJson(`/api/teacher-preferences?semesterId=${encodeURIComponent(preferenceSemester.value)}`);
|
||||
const semester = selectedSemester();
|
||||
preferenceDate.min = semester?.startDate || '';
|
||||
preferenceDate.max = semester?.endDate || '';
|
||||
if (!preferenceDate.value || preferenceDate.value < preferenceDate.min || preferenceDate.value > preferenceDate.max) {
|
||||
preferenceDate.value = semester?.startDate || '';
|
||||
}
|
||||
renderPreferenceGrid();
|
||||
renderPreferenceDates();
|
||||
syncCompactToggles();
|
||||
showPreferenceMessage('', false);
|
||||
} catch (error) {
|
||||
preferenceGrid.innerHTML = `<div class="preference-empty error">${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreferenceGrid() {
|
||||
const slots = preferenceMeta.timeSlots || [];
|
||||
if (!slots.length) {
|
||||
preferenceGrid.innerHTML = '<div class="preference-empty">Базовая сетка времени не настроена</div>';
|
||||
return;
|
||||
}
|
||||
const active = preferences.filter(item => ['PENDING', 'APPROVED'].includes(item.status) && item.dayOfWeek);
|
||||
preferenceGrid.innerHTML = [
|
||||
'<div class="preference-grid-corner">Пара</div>',
|
||||
...['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => `<div class="preference-grid-day">${day}</div>`),
|
||||
...slots.flatMap(slot => [
|
||||
`<div class="preference-grid-time"><strong>${slot.orderNumber}</strong><span>${trimTime(slot.startTime)}–${trimTime(slot.endTime)}</span></div>`,
|
||||
...Array.from({ length: 7 }, (_, index) => {
|
||||
const item = active.find(candidate => candidate.dayOfWeek === index + 1 && candidate.timeSlotId === slot.id);
|
||||
const typeClass = preferenceTypeClass(item?.preferenceType);
|
||||
const pendingClass = item?.status === 'PENDING' ? ' is-pending' : '';
|
||||
const title = item
|
||||
? `${preferenceTypeLabel(item.preferenceType)} · ${preferenceStatusLabel(item.status)}`
|
||||
: 'Добавить отметку';
|
||||
return `<button type="button" class="preference-cell ${typeClass}${pendingClass}"
|
||||
data-day="${index + 1}" data-slot="${slot.id}" data-preference-id="${item?.id || ''}"
|
||||
data-preference-status="${item?.status || ''}" title="${escapeHtml(title)}">
|
||||
<span>${item ? preferenceTypeSymbol(item.preferenceType) : '+'}</span>
|
||||
</button>`;
|
||||
})
|
||||
])
|
||||
].join('');
|
||||
}
|
||||
|
||||
async function handlePreferenceCell(event) {
|
||||
const button = event.target.closest('.preference-cell');
|
||||
if (!button) return;
|
||||
if (button.dataset.preferenceId) {
|
||||
if (button.dataset.preferenceStatus === 'APPROVED') {
|
||||
showPreferenceMessage('Согласованную отметку может снять кафедра или учебный отдел', true);
|
||||
return;
|
||||
}
|
||||
await deletePreference(button.dataset.preferenceId);
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
try {
|
||||
await fetchJson('/api/teacher-preferences', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
semesterId: Number(preferenceSemester.value),
|
||||
preferenceType: document.getElementById('preference-brush').value,
|
||||
dayOfWeek: Number(button.dataset.day),
|
||||
timeSlotId: Number(button.dataset.slot)
|
||||
})
|
||||
});
|
||||
showPreferenceMessage('Отметка отправлена на согласование', false);
|
||||
await loadPreferences();
|
||||
} catch (error) {
|
||||
showPreferenceMessage(error.message, true);
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitUnavailableDate(event) {
|
||||
event.preventDefault();
|
||||
if (!preferenceDate.value) return;
|
||||
try {
|
||||
await fetchJson('/api/teacher-preferences', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
semesterId: Number(preferenceSemester.value),
|
||||
preferenceType: 'HARD_UNAVAILABLE',
|
||||
preferenceDate: preferenceDate.value,
|
||||
comment: preferenceDateComment.value.trim() || null
|
||||
})
|
||||
});
|
||||
preferenceDateComment.value = '';
|
||||
showPreferenceMessage('Дата недоступности отправлена на согласование', false);
|
||||
await loadPreferences();
|
||||
} catch (error) {
|
||||
showPreferenceMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreferenceDates() {
|
||||
const dates = preferences.filter(item => item.preferenceDate && item.status !== 'CANCELLED');
|
||||
preferenceDateList.innerHTML = dates.length ? dates.map(item => `
|
||||
<article>
|
||||
<span class="preference-date-mark ${item.status === 'APPROVED' ? 'approved' : ''}"></span>
|
||||
<div><strong>${escapeHtml(formatIsoDate(item.preferenceDate))}</strong><small>${escapeHtml(item.comment || 'Весь день')}</small></div>
|
||||
<span class="preference-status">${escapeHtml(preferenceStatusLabel(item.status))}</span>
|
||||
${item.status === 'PENDING' ? `<button type="button" class="btn btn-sm btn-danger-subtle" data-cancel-preference="${item.id}">Отменить</button>` : ''}
|
||||
</article>
|
||||
`).join('') : '<div class="preference-empty">Даты полной недоступности не добавлены</div>';
|
||||
}
|
||||
|
||||
async function cancelPreference(event) {
|
||||
const button = event.target.closest('[data-cancel-preference]');
|
||||
if (button) await deletePreference(button.dataset.cancelPreference);
|
||||
}
|
||||
|
||||
async function deletePreference(id) {
|
||||
try {
|
||||
await fetchJson(`/api/teacher-preferences/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||
showPreferenceMessage('Ожидающее пожелание отменено', false);
|
||||
await loadPreferences();
|
||||
} catch (error) {
|
||||
showPreferenceMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function syncCompactToggles() {
|
||||
syncCompactToggle(preferenceConsecutive, 'CONSECUTIVE');
|
||||
syncCompactToggle(preferenceNoGaps, 'NO_GAPS');
|
||||
}
|
||||
|
||||
function syncCompactToggle(input, type) {
|
||||
const item = preferences.find(candidate => candidate.preferenceType === type && ['PENDING', 'APPROVED'].includes(candidate.status));
|
||||
input.checked = Boolean(item);
|
||||
input.dataset.preferenceId = item?.id || '';
|
||||
input.disabled = item?.status === 'APPROVED';
|
||||
input.closest('.preference-toggle-card')?.classList.toggle('approved', item?.status === 'APPROVED');
|
||||
}
|
||||
|
||||
async function toggleCompactPreference(type, input) {
|
||||
try {
|
||||
if (!input.checked && input.dataset.preferenceId) {
|
||||
await fetchJson(`/api/teacher-preferences/${encodeURIComponent(input.dataset.preferenceId)}`, { method: 'DELETE' });
|
||||
} else if (input.checked) {
|
||||
await fetchJson('/api/teacher-preferences', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ semesterId: Number(preferenceSemester.value), preferenceType: type })
|
||||
});
|
||||
}
|
||||
await loadPreferences();
|
||||
} catch (error) {
|
||||
showPreferenceMessage(error.message, true);
|
||||
await loadPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
function showPreferenceMessage(message, isError) {
|
||||
preferenceMessage.textContent = message || '';
|
||||
preferenceMessage.classList.toggle('error', Boolean(isError));
|
||||
preferenceMessage.classList.toggle('success', Boolean(message) && !isError);
|
||||
}
|
||||
|
||||
async function loadChangeRequests() {
|
||||
requestList.innerHTML = '<div class="request-empty">Загрузка заявок...</div>';
|
||||
try {
|
||||
const items = await fetchJson('/api/teacher-change-requests');
|
||||
document.getElementById('change-request-count').textContent = String(items.filter(item => item.status === 'PENDING').length);
|
||||
requestList.innerHTML = items.length ? items.map(renderChangeRequest).join('')
|
||||
: '<div class="request-empty">Заявок на изменение пока нет. Откройте занятие во вкладке «Расписание».</div>';
|
||||
} catch (error) {
|
||||
requestList.innerHTML = `<div class="request-empty error">${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderChangeRequest(item) {
|
||||
const target = item.requestType === 'MOVE'
|
||||
? `${formatIsoDate(item.targetLessonDate || item.lessonDate)}, ${trimTime(item.requestedStartTime)}–${trimTime(item.requestedEndTime)}`
|
||||
: item.requestType === 'CHANGE_CLASSROOM'
|
||||
? item.requestedClassroomName
|
||||
: 'Отмена занятия';
|
||||
const history = (item.history || []).map(entry => `
|
||||
<li><time>${formatInstant(entry.createdAt)}</time><strong>${preferenceStatusLabel(entry.status)}</strong><span>${escapeHtml(entry.comment || 'Без комментария')}</span></li>
|
||||
`).join('');
|
||||
return `<article class="request-ledger-item">
|
||||
<div class="request-ledger-stripe ${String(item.status || '').toLowerCase()}"></div>
|
||||
<div class="request-ledger-main">
|
||||
<span>${escapeHtml(formatIsoDate(item.lessonDate))} · ${trimTime(item.sourceStartTime)}–${trimTime(item.sourceEndTime)}</span>
|
||||
<h3>${escapeHtml(item.subjectName)}</h3>
|
||||
<p>${escapeHtml((item.groupNames || []).join(', '))} · ${escapeHtml(item.sourceClassroomName)}</p>
|
||||
<div class="request-change-line"><b>${escapeHtml(changeTypeLabel(item.requestType))}</b><i>→</i><strong>${escapeHtml(target)}</strong></div>
|
||||
<p class="request-reason">${escapeHtml(item.reason)}</p>
|
||||
${item.reviewComment ? `<p class="request-review-comment">Решение: ${escapeHtml(item.reviewComment)}</p>` : ''}
|
||||
<details><summary>История решения · ${(item.history || []).length}</summary><ol>${history}</ol></details>
|
||||
</div>
|
||||
<div class="request-ledger-side">
|
||||
<span class="request-status ${String(item.status || '').toLowerCase()}">${escapeHtml(preferenceStatusLabel(item.status))}</span>
|
||||
${item.status === 'PENDING' ? `<button type="button" class="btn btn-sm btn-danger-subtle" data-cancel-request="${item.id}">Отозвать</button>` : ''}
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
async function openRequestDialog(key) {
|
||||
activeRequestLesson = scheduleLessons.get(key);
|
||||
if (!activeRequestLesson) return;
|
||||
requestOverlay.hidden = false;
|
||||
document.body.classList.add('dialog-open');
|
||||
document.getElementById('request-dialog-lesson').textContent = `${activeRequestLesson.subjectName} · ${formatIsoDate(activeRequestLesson.date)} · ${trimTime(activeRequestLesson.startTime)}–${trimTime(activeRequestLesson.endTime)}`;
|
||||
requestReason.value = '';
|
||||
requestType.value = 'MOVE';
|
||||
setRequestMessage('Проверяем допустимые варианты...', false);
|
||||
try {
|
||||
activeCandidates = await fetchJson(`/api/teacher-change-requests/candidates?baseRuleSlotId=${encodeURIComponent(activeRequestLesson.scheduleRuleSlotId)}&lessonDate=${encodeURIComponent(activeRequestLesson.date)}`);
|
||||
renderCandidateDates();
|
||||
renderCandidateOptions();
|
||||
updateRequestFields();
|
||||
setRequestMessage('Доступны только варианты, прошедшие проверку конфликтов', false);
|
||||
} catch (error) {
|
||||
setRequestMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function closeRequestDialog() {
|
||||
requestOverlay.hidden = true;
|
||||
document.body.classList.remove('dialog-open');
|
||||
activeRequestLesson = null;
|
||||
activeCandidates = null;
|
||||
}
|
||||
|
||||
function renderCandidateDates() {
|
||||
requestDate.innerHTML = (activeCandidates?.availableDates || []).map(date => `
|
||||
<option value="${date}" ${date === activeCandidates.checkedDate ? 'selected' : ''}>${formatIsoDate(date)}</option>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderCandidateOptions() {
|
||||
requestTime.innerHTML = candidateOptions(activeCandidates?.timeSlots, 'Нет свободных временных слотов');
|
||||
requestClassroom.innerHTML = candidateOptions(activeCandidates?.classrooms, 'Нет свободных аудиторий');
|
||||
}
|
||||
|
||||
function candidateOptions(items, emptyLabel) {
|
||||
const available = (items || []).filter(item => item.available);
|
||||
return available.length
|
||||
? available.map(item => `<option value="${item.id}">${escapeHtml(item.label)}</option>`).join('')
|
||||
: `<option value="">${emptyLabel}</option>`;
|
||||
}
|
||||
|
||||
async function reloadRequestCandidates() {
|
||||
if (!activeRequestLesson || !requestDate.value) return;
|
||||
setRequestMessage('Проверяем выбранную дату...', false);
|
||||
try {
|
||||
activeCandidates = await fetchJson(`/api/teacher-change-requests/candidates?baseRuleSlotId=${encodeURIComponent(activeRequestLesson.scheduleRuleSlotId)}&lessonDate=${encodeURIComponent(activeRequestLesson.date)}&targetDate=${encodeURIComponent(requestDate.value)}`);
|
||||
renderCandidateOptions();
|
||||
setRequestMessage('Предварительная проверка завершена', false);
|
||||
} catch (error) {
|
||||
setRequestMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRequestFields() {
|
||||
document.querySelector('[data-request-field="date"]').hidden = requestType.value !== 'MOVE';
|
||||
document.querySelector('[data-request-field="time"]').hidden = requestType.value !== 'MOVE';
|
||||
document.querySelector('[data-request-field="classroom"]').hidden = requestType.value !== 'CHANGE_CLASSROOM';
|
||||
}
|
||||
|
||||
async function submitChangeRequest(event) {
|
||||
event.preventDefault();
|
||||
if (!activeRequestLesson) return;
|
||||
if (!requestReason.value.trim()) {
|
||||
setRequestMessage('Укажите обоснование заявки', true);
|
||||
return;
|
||||
}
|
||||
const type = requestType.value;
|
||||
const payload = {
|
||||
baseRuleSlotId: activeRequestLesson.scheduleRuleSlotId,
|
||||
lessonDate: activeRequestLesson.date,
|
||||
requestType: type,
|
||||
targetLessonDate: type === 'MOVE' ? requestDate.value : null,
|
||||
requestedTimeSlotId: type === 'MOVE' ? Number(requestTime.value) || null : null,
|
||||
requestedClassroomId: type === 'CHANGE_CLASSROOM' ? Number(requestClassroom.value) || null : null,
|
||||
reason: requestReason.value.trim()
|
||||
};
|
||||
try {
|
||||
await fetchJson('/api/teacher-change-requests', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
closeRequestDialog();
|
||||
await loadChangeRequests();
|
||||
switchWorkspace('requests');
|
||||
} catch (error) {
|
||||
setRequestMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelChangeRequest(event) {
|
||||
const button = event.target.closest('[data-cancel-request]');
|
||||
if (!button) return;
|
||||
try {
|
||||
await fetchJson(`/api/teacher-change-requests/${encodeURIComponent(button.dataset.cancelRequest)}`, { method: 'DELETE' });
|
||||
await loadChangeRequests();
|
||||
} catch (error) {
|
||||
requestList.insertAdjacentHTML('afterbegin', `<div class="request-empty error">${escapeHtml(error.message)}</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
function setRequestMessage(message, isError) {
|
||||
requestMessage.textContent = message || '';
|
||||
requestMessage.classList.toggle('error', Boolean(isError));
|
||||
}
|
||||
|
||||
async function loadAbsences() {
|
||||
absenceList.innerHTML = '<div class="teacher-absence-empty">Загрузка заявок...</div>';
|
||||
@@ -152,6 +543,7 @@ import {
|
||||
renderEmptyWeek('Загрузка...');
|
||||
try {
|
||||
const lessons = await fetchJson(`/api/schedule?teacherId=${encodeURIComponent(teacherId)}&startDate=${startDate}&endDate=${endDate}`);
|
||||
scheduleLessons = new Map(lessons.map(lesson => [lessonKey(lesson), lesson]));
|
||||
renderWeek(lessons);
|
||||
showStatus(lessons.length ? `Найдено занятий: ${lessons.length}` : 'На этой неделе занятий нет', false);
|
||||
} catch (error) {
|
||||
@@ -249,10 +641,19 @@ import {
|
||||
<span class="chip">${escapeHtml(lesson.classroomName)}</span>
|
||||
${groups ? `<span class="chip">${escapeHtml(groups)}</span>` : ''}
|
||||
</div>
|
||||
${lesson.scheduleRuleSlotId && !lesson.scheduleOverrideId ? `
|
||||
<button class="lesson-request-button" type="button" data-request-lesson="${escapeHtml(lessonKey(lesson))}">
|
||||
Запросить изменение
|
||||
</button>
|
||||
` : '<span class="lesson-change-note">Занятие уже изменено учебным отделом</span>'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function lessonKey(lesson) {
|
||||
return `${lesson.scheduleRuleSlotId}:${lesson.date}`;
|
||||
}
|
||||
|
||||
function collectSlots(lessons) {
|
||||
const slots = new Map();
|
||||
lessons.forEach(lesson => {
|
||||
@@ -335,6 +736,59 @@ import {
|
||||
return `is-${String(status || '').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function selectedSemester() {
|
||||
return (preferenceMeta.semesters || []).find(item => String(item.id) === String(preferenceSemester.value));
|
||||
}
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = semester.semesterType === 'AUTUMN' ? 'осенний' : semester.semesterType === 'SPRING' ? 'весенний' : semester.semesterType;
|
||||
return `${semester.academicYearTitle} · ${type} · ${formatIsoDate(semester.startDate)}–${formatIsoDate(semester.endDate)}`;
|
||||
}
|
||||
|
||||
function preferenceTypeLabel(type) {
|
||||
return {
|
||||
HARD_UNAVAILABLE: 'Строго недоступен',
|
||||
SOFT_PREFERRED: 'Предпочтительно',
|
||||
SOFT_UNWANTED: 'Нежелательно',
|
||||
CONSECUTIVE: 'Пары подряд',
|
||||
NO_GAPS: 'Без окон'
|
||||
}[type] || type || '—';
|
||||
}
|
||||
|
||||
function preferenceTypeClass(type) {
|
||||
return {
|
||||
HARD_UNAVAILABLE: 'is-hard',
|
||||
SOFT_PREFERRED: 'is-preferred',
|
||||
SOFT_UNWANTED: 'is-unwanted'
|
||||
}[type] || '';
|
||||
}
|
||||
|
||||
function preferenceTypeSymbol(type) {
|
||||
return { HARD_UNAVAILABLE: '×', SOFT_PREFERRED: '✓', SOFT_UNWANTED: '!' }[type] || '•';
|
||||
}
|
||||
|
||||
function preferenceStatusLabel(status) {
|
||||
return {
|
||||
PENDING: 'На рассмотрении',
|
||||
APPROVED: 'Согласовано',
|
||||
REJECTED: 'Отклонено',
|
||||
CANCELLED: 'Отменено'
|
||||
}[status] || status || '—';
|
||||
}
|
||||
|
||||
function changeTypeLabel(type) {
|
||||
return {
|
||||
MOVE: 'Перенос',
|
||||
CHANGE_CLASSROOM: 'Смена аудитории',
|
||||
CANCEL: 'Отмена'
|
||||
}[type] || type || '—';
|
||||
}
|
||||
|
||||
function formatInstant(value) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
|
||||
}
|
||||
|
||||
function dayName(date) {
|
||||
const value = date.toLocaleDateString('ru-RU', { weekday: 'long' });
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Расписание преподавателя</title>
|
||||
<title>Кабинет преподавателя</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div class="title">
|
||||
<h1>Расписание преподавателя</h1>
|
||||
<h1>Кабинет преподавателя</h1>
|
||||
<span id="week-label">Загрузка периода...</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
@@ -23,6 +23,15 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="workspace-nav" aria-label="Разделы кабинета">
|
||||
<button class="workspace-tab active" type="button" data-workspace-tab="schedule">Расписание</button>
|
||||
<button class="workspace-tab" type="button" data-workspace-tab="preferences">Мои пожелания</button>
|
||||
<button class="workspace-tab" type="button" data-workspace-tab="requests">
|
||||
Заявки на изменение <span id="change-request-count">0</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section class="workspace-view active" data-workspace-view="schedule">
|
||||
<section class="toolbar">
|
||||
<div class="week-nav">
|
||||
<button class="btn btn-icon-md btn-secondary" id="prev-week" type="button" aria-label="Предыдущая неделя">
|
||||
@@ -71,8 +80,133 @@
|
||||
<div class="teacher-absence-list" id="teacher-absence-list" aria-live="polite"></div>
|
||||
</section>
|
||||
<section class="schedule" id="schedule-grid" aria-live="polite"></section>
|
||||
</section>
|
||||
|
||||
<section class="workspace-view" data-workspace-view="preferences" hidden>
|
||||
<section class="preference-hero">
|
||||
<div>
|
||||
<span class="preference-kicker">Контур доступности</span>
|
||||
<h2>Соберите ритм семестра до составления расписания</h2>
|
||||
<p>Строгая недоступность блокирует назначение. Мягкие отметки и пожелания к компактности помогают учебному отделу выбрать удобный вариант.</p>
|
||||
</div>
|
||||
<label class="preference-semester-field">
|
||||
<span>Семестр</span>
|
||||
<select id="preference-semester"></select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="preference-board">
|
||||
<header class="preference-board-head">
|
||||
<div>
|
||||
<span>Недельный шаблон</span>
|
||||
<h3>Отмечайте интервалы одним касанием</h3>
|
||||
</div>
|
||||
<label>
|
||||
<span>Режим отметки</span>
|
||||
<select id="preference-brush">
|
||||
<option value="SOFT_PREFERRED">Предпочтительно</option>
|
||||
<option value="SOFT_UNWANTED">Нежелательно</option>
|
||||
<option value="HARD_UNAVAILABLE">Строго недоступен</option>
|
||||
</select>
|
||||
</label>
|
||||
</header>
|
||||
<div class="preference-legend" aria-label="Легенда">
|
||||
<span class="is-preferred">Предпочтительно</span>
|
||||
<span class="is-unwanted">Нежелательно</span>
|
||||
<span class="is-hard">Строгий запрет</span>
|
||||
<span class="is-pending">Ожидает согласования</span>
|
||||
</div>
|
||||
<div class="preference-grid-wrap">
|
||||
<div class="preference-grid" id="preference-grid" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="preference-message" id="preference-message" role="status"></div>
|
||||
</section>
|
||||
|
||||
<div class="preference-lower-grid">
|
||||
<section class="preference-card">
|
||||
<span class="preference-card-number">01</span>
|
||||
<h3>Полная недоступность</h3>
|
||||
<p>Добавьте отдельную дату внутри семестра — все пары этого дня будут считаться строгим ограничением.</p>
|
||||
<form id="preference-date-form" class="preference-date-form">
|
||||
<input type="date" id="preference-date" required>
|
||||
<input type="text" id="preference-date-comment" maxlength="2000" placeholder="Комментарий (необязательно)">
|
||||
<button class="btn btn-md btn-primary" type="submit">Добавить дату</button>
|
||||
</form>
|
||||
<div class="preference-date-list" id="preference-date-list"></div>
|
||||
</section>
|
||||
<section class="preference-card">
|
||||
<span class="preference-card-number">02</span>
|
||||
<h3>Компактность недели</h3>
|
||||
<p>Эти мягкие пожелания не отменяют занятия автоматически, но видны при составлении правил.</p>
|
||||
<label class="preference-toggle-card">
|
||||
<input type="checkbox" id="preference-consecutive">
|
||||
<span><strong>Пары подряд</strong><small>Собирать занятия последовательными блоками</small></span>
|
||||
</label>
|
||||
<label class="preference-toggle-card">
|
||||
<input type="checkbox" id="preference-no-gaps">
|
||||
<span><strong>Без окон</strong><small>Не оставлять пустые интервалы между занятиями</small></span>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="workspace-view" data-workspace-view="requests" hidden>
|
||||
<section class="request-ledger-head">
|
||||
<div>
|
||||
<span>Журнал решений</span>
|
||||
<h2>Заявки на изменение занятий</h2>
|
||||
<p>Откройте занятие в расписании, чтобы запросить перенос, другую аудиторию или отмену. Здесь сохраняются статус, комментарий и вся история решения.</p>
|
||||
</div>
|
||||
<button class="btn btn-md btn-secondary" id="change-request-refresh" type="button">Обновить</button>
|
||||
</section>
|
||||
<div class="request-ledger" id="change-request-list" aria-live="polite"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="request-dialog-overlay" id="request-dialog-overlay" hidden>
|
||||
<section class="request-dialog" role="dialog" aria-modal="true" aria-labelledby="request-dialog-title">
|
||||
<header>
|
||||
<div>
|
||||
<span>Предварительная проверка</span>
|
||||
<h2 id="request-dialog-title">Заявка на изменение</h2>
|
||||
<p id="request-dialog-lesson">Загрузка занятия...</p>
|
||||
</div>
|
||||
<button class="btn btn-icon-md btn-secondary" id="request-dialog-close" type="button" aria-label="Закрыть">×</button>
|
||||
</header>
|
||||
<form id="change-request-form">
|
||||
<label>
|
||||
<span>Что изменить</span>
|
||||
<select id="change-request-type">
|
||||
<option value="MOVE">Перенести дату или время</option>
|
||||
<option value="CHANGE_CLASSROOM">Сменить аудиторию</option>
|
||||
<option value="CANCEL">Отменить занятие</option>
|
||||
</select>
|
||||
</label>
|
||||
<label data-request-field="date">
|
||||
<span>Новая дата</span>
|
||||
<select id="change-request-date"></select>
|
||||
</label>
|
||||
<label data-request-field="time">
|
||||
<span>Новая пара</span>
|
||||
<select id="change-request-time"></select>
|
||||
</label>
|
||||
<label data-request-field="classroom" hidden>
|
||||
<span>Новая аудитория</span>
|
||||
<select id="change-request-classroom"></select>
|
||||
</label>
|
||||
<label class="request-reason-field">
|
||||
<span>Обоснование</span>
|
||||
<textarea id="change-request-reason" maxlength="2000" rows="4" placeholder="Почему требуется изменение" required></textarea>
|
||||
</label>
|
||||
<div class="request-dialog-message" id="request-dialog-message" role="alert"></div>
|
||||
<footer>
|
||||
<button class="btn btn-md btn-secondary" id="request-dialog-cancel" type="button">Закрыть</button>
|
||||
<button class="btn btn-md btn-primary" type="submit">Отправить в учебный отдел</button>
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -185,6 +185,8 @@
|
||||
--green: #047857;
|
||||
--orange: #b45309;
|
||||
--shadow: 0 12px 30px rgba(23, 32, 51, 0.08);
|
||||
--card: var(--panel);
|
||||
--border: var(--panel-border);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -419,6 +421,456 @@ body {
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workspace-nav {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
margin: 0 0 18px;
|
||||
padding: 5px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.workspace-tab {
|
||||
min-height: 38px;
|
||||
padding: 0 15px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.workspace-tab span {
|
||||
display: inline-grid;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
margin-left: 5px;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.workspace-tab.active {
|
||||
background: var(--text);
|
||||
color: var(--panel);
|
||||
}
|
||||
|
||||
.workspace-view[hidden],
|
||||
.request-dialog-overlay[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workspace-view.active {
|
||||
animation: workspace-in 0.28s ease both;
|
||||
}
|
||||
|
||||
@keyframes workspace-in {
|
||||
from { opacity: 0; transform: translateY(7px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.preference-hero,
|
||||
.request-ledger-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 30px;
|
||||
margin-bottom: 14px;
|
||||
padding: 26px;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--panel-border));
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(120deg, color-mix(in srgb, var(--accent) 13%, transparent), transparent 58%),
|
||||
var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.preference-hero::after {
|
||||
position: absolute;
|
||||
top: -90px;
|
||||
right: 18%;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 23%, transparent);
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.preference-kicker,
|
||||
.request-ledger-head > div > span {
|
||||
color: var(--accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preference-hero h2,
|
||||
.request-ledger-head h2 {
|
||||
max-width: 720px;
|
||||
margin: 7px 0;
|
||||
font-size: clamp(24px, 4vw, 42px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.035em;
|
||||
}
|
||||
|
||||
.preference-hero p,
|
||||
.request-ledger-head p {
|
||||
max-width: 760px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.preference-semester-field,
|
||||
.preference-board-head label,
|
||||
.request-dialog label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preference-semester-field {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.preference-semester-field select,
|
||||
.preference-board-head select,
|
||||
.preference-date-form input,
|
||||
.request-dialog select,
|
||||
.request-dialog textarea {
|
||||
min-height: 42px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 9px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.preference-board,
|
||||
.preference-card {
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 16px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.preference-board {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preference-board-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.preference-board-head > div > span,
|
||||
.preference-card-number {
|
||||
color: var(--accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.preference-board-head h3,
|
||||
.preference-card h3 {
|
||||
margin: 5px 0 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.preference-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 16px 0 10px;
|
||||
}
|
||||
|
||||
.preference-legend span {
|
||||
padding: 4px 9px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preference-legend .is-preferred { border-color: #10b981; color: #047857; }
|
||||
.preference-legend .is-unwanted { border-color: #f59e0b; color: #b45309; }
|
||||
.preference-legend .is-hard { border-color: #f43f5e; color: #be123c; }
|
||||
.preference-legend .is-pending { border-style: dashed; }
|
||||
|
||||
.preference-grid-wrap {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.preference-grid {
|
||||
display: grid;
|
||||
min-width: 780px;
|
||||
grid-template-columns: 110px repeat(7, minmax(78px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preference-grid-corner,
|
||||
.preference-grid-day,
|
||||
.preference-grid-time,
|
||||
.preference-cell {
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.preference-grid-corner,
|
||||
.preference-grid-day {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: color-mix(in srgb, var(--panel-border) 22%, transparent);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.preference-grid-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.preference-grid-time strong {
|
||||
display: grid;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
place-items: center;
|
||||
border-radius: 7px;
|
||||
background: var(--text);
|
||||
color: var(--panel);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preference-grid-time span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.preference-cell {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: transform .16s ease, border-color .16s ease, background .16s ease;
|
||||
}
|
||||
|
||||
.preference-cell:hover { transform: translateY(-2px); border-color: var(--accent); }
|
||||
.preference-cell span { font-size: 19px; font-weight: 900; }
|
||||
.preference-cell.is-preferred { border-color: #10b981; background: color-mix(in srgb, #10b981 15%, transparent); color: #047857; }
|
||||
.preference-cell.is-unwanted { border-color: #f59e0b; background: color-mix(in srgb, #f59e0b 15%, transparent); color: #b45309; }
|
||||
.preference-cell.is-hard { border-color: #f43f5e; background: color-mix(in srgb, #f43f5e 14%, transparent); color: #be123c; }
|
||||
.preference-cell.is-pending::after {
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border: 1px dashed currentColor;
|
||||
border-radius: 6px;
|
||||
content: '';
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.preference-message,
|
||||
.request-dialog-message {
|
||||
min-height: 20px;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.preference-message.success { color: #059669; }
|
||||
.preference-message.error,
|
||||
.request-dialog-message.error { color: #dc2626; }
|
||||
|
||||
.preference-lower-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.35fr .65fr;
|
||||
gap: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.preference-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preference-card p {
|
||||
margin: 7px 0 16px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.preference-date-form {
|
||||
display: grid;
|
||||
grid-template-columns: 170px 1fr auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preference-date-list {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.preference-date-list article {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
gap: 9px;
|
||||
align-items: center;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.preference-date-list small { display: block; margin-top: 2px; color: var(--muted); }
|
||||
.preference-date-mark { width: 7px; height: 34px; border-radius: 9px; background: #f59e0b; }
|
||||
.preference-date-mark.approved { background: #f43f5e; }
|
||||
.preference-status { color: var(--muted); font-size: 11px; font-weight: 800; }
|
||||
.preference-empty { padding: 14px; color: var(--muted); text-align: center; font-size: 13px; }
|
||||
|
||||
.preference-toggle-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
margin-top: 9px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.preference-toggle-card.approved { border-color: #10b981; background: color-mix(in srgb, #10b981 8%, transparent); }
|
||||
.preference-toggle-card input { margin-top: 3px; accent-color: var(--accent); }
|
||||
.preference-toggle-card strong,
|
||||
.preference-toggle-card small { display: block; }
|
||||
.preference-toggle-card small { margin-top: 3px; color: var(--muted); line-height: 1.4; }
|
||||
|
||||
.lesson-request-button {
|
||||
width: 100%;
|
||||
margin-top: 9px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--green) 35%, var(--panel-border));
|
||||
border-radius: 7px;
|
||||
background: var(--panel);
|
||||
color: var(--green);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lesson-change-note { display: block; margin-top: 8px; color: var(--muted); font-size: 10px; }
|
||||
|
||||
.request-ledger-head h2 { font-size: clamp(24px, 3vw, 34px); }
|
||||
.request-ledger { display: grid; gap: 10px; }
|
||||
.request-empty { padding: 28px; border: 1px dashed var(--panel-border); border-radius: 14px; color: var(--muted); text-align: center; }
|
||||
|
||||
.request-ledger-item {
|
||||
display: grid;
|
||||
grid-template-columns: 7px minmax(0, 1fr) auto;
|
||||
gap: 17px;
|
||||
padding: 17px;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 15px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.request-ledger-stripe { border-radius: 99px; background: #94a3b8; }
|
||||
.request-ledger-stripe.pending { background: #f59e0b; }
|
||||
.request-ledger-stripe.approved { background: #10b981; }
|
||||
.request-ledger-stripe.rejected { background: #f43f5e; }
|
||||
.request-ledger-main > span { color: var(--muted); font-size: 12px; }
|
||||
.request-ledger-main h3 { margin: 4px 0; }
|
||||
.request-ledger-main p { margin: 3px 0; color: var(--muted); font-size: 13px; }
|
||||
.request-change-line { display: flex; align-items: center; gap: 9px; margin: 12px 0; font-size: 13px; }
|
||||
.request-change-line b { color: var(--accent-strong); }
|
||||
.request-change-line i { color: var(--muted); font-style: normal; }
|
||||
.request-ledger-main .request-reason { color: var(--text); }
|
||||
.request-ledger-main .request-review-comment { color: #047857; }
|
||||
.request-ledger-main details { margin-top: 10px; }
|
||||
.request-ledger-main summary { color: var(--muted); font-size: 12px; cursor: pointer; }
|
||||
.request-ledger-main ol { display: grid; gap: 5px; padding-left: 20px; }
|
||||
.request-ledger-main li { padding-left: 5px; font-size: 12px; }
|
||||
.request-ledger-main li time,
|
||||
.request-ledger-main li strong,
|
||||
.request-ledger-main li span { margin-right: 8px; }
|
||||
.request-ledger-main li time { color: var(--muted); }
|
||||
.request-ledger-side { display: flex; flex-direction: column; align-items: end; gap: 9px; }
|
||||
.request-status { padding: 5px 9px; border: 1px solid currentColor; border-radius: 999px; color: var(--muted); font-size: 11px; font-weight: 800; }
|
||||
.request-status.pending { color: #b45309; }
|
||||
.request-status.approved { color: #047857; }
|
||||
.request-status.rejected { color: #be123c; }
|
||||
|
||||
.request-dialog-overlay {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(7, 12, 22, .68);
|
||||
backdrop-filter: blur(7px);
|
||||
}
|
||||
|
||||
.dialog-open { overflow: hidden; }
|
||||
.request-dialog {
|
||||
width: min(760px, 100%);
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 18px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 35px 90px rgba(0, 0, 0, .35);
|
||||
}
|
||||
|
||||
.request-dialog header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
background: linear-gradient(115deg, color-mix(in srgb, var(--accent) 12%, transparent), transparent);
|
||||
}
|
||||
|
||||
.request-dialog header span { color: var(--accent-strong); font-size: 11px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.request-dialog header h2 { margin: 5px 0; }
|
||||
.request-dialog header p { margin: 0; color: var(--muted); font-size: 13px; }
|
||||
.request-dialog form { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; padding: 20px; }
|
||||
.request-dialog [hidden] { display: none; }
|
||||
.request-dialog textarea { min-height: 100px; padding-top: 10px; resize: vertical; }
|
||||
.request-reason-field,
|
||||
.request-dialog-message,
|
||||
.request-dialog footer { grid-column: 1 / -1; }
|
||||
.request-dialog footer { display: flex; justify-content: flex-end; gap: 8px; padding-top: 5px; }
|
||||
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
@@ -471,5 +923,26 @@ body {
|
||||
.week-nav .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workspace-nav,
|
||||
.preference-hero,
|
||||
.preference-board-head,
|
||||
.request-ledger-head {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.workspace-tab { text-align: left; }
|
||||
.preference-semester-field { min-width: 0; }
|
||||
.preference-lower-grid,
|
||||
.preference-date-form,
|
||||
.request-dialog form { grid-template-columns: 1fr; }
|
||||
.preference-date-form .btn,
|
||||
.request-reason-field,
|
||||
.request-dialog-message,
|
||||
.request-dialog footer { grid-column: auto; }
|
||||
.request-ledger-item { grid-template-columns: 6px minmax(0, 1fr); }
|
||||
.request-ledger-side { grid-column: 2; align-items: start; }
|
||||
.request-dialog footer { flex-direction: column-reverse; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user